@effected/github 0.23.1 → 0.25.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 CHANGED
@@ -191,10 +191,23 @@ const testClaims = (overrides = {}) => ({
191
191
  *
192
192
  * @public
193
193
  */
194
- const ActionsOidc = {
195
- issuer: ISSUER,
196
- verify,
197
- testClaims
194
+ var ActionsOidc = class {
195
+ constructor() {}
196
+ /** The Actions OIDC issuer, `https://token.actions.githubusercontent.com`. */
197
+ static issuer = ISSUER;
198
+ /** Verify a runner-issued OIDC token: signature against GitHub's JWKS, iss, aud, exp/nbf, then typed claims. */
199
+ static verify = verify;
200
+ /**
201
+ * A valid claims payload for tests, overridable field by field.
202
+ *
203
+ * @remarks
204
+ * Wire-shaped (snake_case keys, string ids) and complete except for the
205
+ * time claims `iat` and `exp`, which depend on the clock: a signer stamps
206
+ * them, as `TestIssuer.sign` from `@effected/jwt/testing` does. `iss` is
207
+ * {@link ActionsOidc.issuer}; `aud` is a placeholder to override with the
208
+ * audience under test.
209
+ */
210
+ static testClaims = testClaims;
198
211
  };
199
212
 
200
213
  export { ActionsOidc, ActionsOidcClaims, ActionsOidcClaimsFromWire };
@@ -1,3 +1,4 @@
1
+ import { unstubbed } from "./internal/unstubbed.js";
1
2
  import { GitHubClient } from "./GitHubClient.js";
2
3
  import { Repo } from "./Repo.js";
3
4
  import { Context, Effect, Layer, Schema } from "effect";
@@ -37,14 +38,12 @@ var StorageRecordInput = class extends Schema.Class("StorageRecordInput")({
37
38
  var ArtifactMetadata = class ArtifactMetadata extends Context.Service()("@effected/github/ArtifactMetadata") {
38
39
  /** The live service, built over a `GitHubClient`. */
39
40
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
40
- /** An in-memory double; unstubbed members die naming themselves. */
41
- static makeTest = (overrides = {}) => ({ createStorageRecord: overrides.createStorageRecord ?? (() => unstubbed("createStorageRecord")) });
41
+ /** An in-memory double; an unstubbed member dies when run, naming itself. */
42
+ static makeTest = (overrides = {}) => ({ createStorageRecord: overrides.createStorageRecord ?? (() => dies("createStorageRecord")) });
42
43
  /** {@link ArtifactMetadata.makeTest} behind a `Layer`. */
43
44
  static layerTest = (overrides = {}) => Layer.succeed(ArtifactMetadata, ArtifactMetadata.makeTest(overrides));
44
45
  };
45
- const unstubbed = (member) => {
46
- throw new Error(`ArtifactMetadata.makeTest: ${member}() was called but not stubbed — pass an override.`);
47
- };
46
+ const dies = unstubbed("ArtifactMetadata.makeTest");
48
47
  const make = (client) => ({ createStorageRecord: Effect.fn("ArtifactMetadata.createStorageRecord")(function* (input) {
49
48
  const { owner } = yield* Repo;
50
49
  yield* Effect.annotateCurrentSpan({
package/Attestation.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { unstubbed } from "./internal/unstubbed.js";
1
2
  import { GitHubClient } from "./GitHubClient.js";
2
3
  import { Repo } from "./Repo.js";
3
4
  import { Context, Effect, Layer, Schema } from "effect";
@@ -11,6 +12,10 @@ import { Context, Effect, Layer, Schema } from "effect";
11
12
  * with a stated sunset. Pinning the version means the response is on a contract
12
13
  * the generated types do not describe — which is why this module is the one that
13
14
  * uses `requestDecoded` with owned schemas rather than the route table.
15
+ *
16
+ * Sent as a per-call header, which wins over the client's own
17
+ * `apiVersion`, so these reads stay on this contract even when a consumer
18
+ * pins the client to another version.
14
19
  */
15
20
  const API_VERSION = "2026-03-10";
16
21
  /**
@@ -54,17 +59,15 @@ const ListResponse = Schema.Struct({ attestations: Schema.optionalKey(Schema.Arr
54
59
  var Attestation = class Attestation extends Context.Service()("@effected/github/Attestation") {
55
60
  /** The live service, built over a `GitHubClient`. */
56
61
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
57
- /** An in-memory double; unstubbed members die naming themselves. */
62
+ /** An in-memory double; an unstubbed member dies when run, naming itself. */
58
63
  static makeTest = (overrides = {}) => ({
59
- upload: overrides.upload ?? (() => unstubbed("upload")),
60
- listForSubject: overrides.listForSubject ?? (() => unstubbed("listForSubject"))
64
+ upload: overrides.upload ?? (() => dies("upload")),
65
+ listForSubject: overrides.listForSubject ?? (() => dies("listForSubject"))
61
66
  });
62
67
  /** {@link Attestation.makeTest} behind a `Layer`. */
63
68
  static layerTest = (overrides = {}) => Layer.succeed(Attestation, Attestation.makeTest(overrides));
64
69
  };
65
- const unstubbed = (member) => {
66
- throw new Error(`Attestation.makeTest: ${member}() was called but not stubbed — pass an override.`);
67
- };
70
+ const dies = unstubbed("Attestation.makeTest");
68
71
  const make = (client) => ({
69
72
  upload: Effect.fn("Attestation.upload")(function* (bundle) {
70
73
  const { owner, repo } = yield* Repo;
@@ -1,4 +1,5 @@
1
1
  import { GitHubError } from "./GitHubError.js";
2
+ import { unstubbed } from "./internal/unstubbed.js";
2
3
  import { GitHubClient } from "./GitHubClient.js";
3
4
  import { numericId } from "./internal/ids.js";
4
5
  import { Context, Effect, Layer, Schema, Stream } from "effect";
@@ -118,20 +119,18 @@ var DeliveryAttempt = class extends Schema.Class("DeliveryAttempt")({
118
119
  var AuthenticatedApp = class AuthenticatedApp extends Context.Service()("@effected/github/AuthenticatedApp") {
119
120
  /** The live service, built over an App-JWT `GitHubClient`. */
120
121
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
121
- /** An in-memory double; unstubbed members die naming themselves. */
122
+ /** An in-memory double; an unstubbed member dies when run, naming itself. */
122
123
  static makeTest = (overrides = {}) => ({
123
- get: overrides.get ?? (() => unstubbed("get")),
124
- deliveries: overrides.deliveries ?? (() => unstubbed("deliveries")),
125
- delivery: overrides.delivery ?? (() => unstubbed("delivery")),
126
- redeliver: overrides.redeliver ?? (() => unstubbed("redeliver")),
127
- uninstall: overrides.uninstall ?? (() => unstubbed("uninstall"))
124
+ get: overrides.get ?? (() => dies("get")),
125
+ deliveries: overrides.deliveries ?? (() => Stream.fromEffect(dies("deliveries"))),
126
+ delivery: overrides.delivery ?? (() => dies("delivery")),
127
+ redeliver: overrides.redeliver ?? (() => dies("redeliver")),
128
+ uninstall: overrides.uninstall ?? (() => dies("uninstall"))
128
129
  });
129
130
  /** {@link AuthenticatedApp.makeTest} behind a `Layer`. */
130
131
  static layerTest = (overrides = {}) => Layer.succeed(AuthenticatedApp, AuthenticatedApp.makeTest(overrides));
131
132
  };
132
- const unstubbed = (member) => {
133
- throw new Error(`AuthenticatedApp.makeTest: ${member}() was called but not stubbed — pass an override.`);
134
- };
133
+ const dies = unstubbed("AuthenticatedApp.makeTest");
135
134
  const decodeDelivery = Schema.decodeUnknownEffect(DeliveryAttempt);
136
135
  const decodeApp = Schema.decodeUnknownEffect(AppInfo);
137
136
  /** A nullable int64 id, narrowed like every other id. */
package/CheckRun.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { GitHubError } from "./GitHubError.js";
2
+ import { unstubbed } from "./internal/unstubbed.js";
2
3
  import { GitHubClient } from "./GitHubClient.js";
3
4
  import { Repo } from "./Repo.js";
4
5
  import { numericId } from "./internal/ids.js";
@@ -239,24 +240,22 @@ var CheckRunRef = class extends Schema.Class("CheckRunRef")({
239
240
  var CheckRun = class CheckRun extends Context.Service()("@effected/github/CheckRun") {
240
241
  /** The live service, built over a `GitHubClient`. */
241
242
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
242
- /** An in-memory double; unstubbed members die naming themselves. */
243
+ /** An in-memory double; an unstubbed member dies when run, naming itself. */
243
244
  static makeTest = (overrides = {}) => ({
244
- create: overrides.create ?? (() => unstubbed("create")),
245
- get: overrides.get ?? (() => unstubbed("get")),
246
- update: overrides.update ?? (() => unstubbed("update")),
247
- updateRef: overrides.updateRef ?? (() => unstubbed("updateRef")),
248
- list: overrides.list ?? (() => unstubbed("list")),
249
- findByExternalId: overrides.findByExternalId ?? (() => unstubbed("findByExternalId")),
250
- complete: overrides.complete ?? (() => unstubbed("complete")),
251
- completeRef: overrides.completeRef ?? (() => unstubbed("completeRef")),
252
- withCheckRun: overrides.withCheckRun ?? (() => unstubbed("withCheckRun"))
245
+ create: overrides.create ?? (() => dies("create")),
246
+ get: overrides.get ?? (() => dies("get")),
247
+ update: overrides.update ?? (() => dies("update")),
248
+ updateRef: overrides.updateRef ?? (() => dies("updateRef")),
249
+ list: overrides.list ?? (() => dies("list")),
250
+ findByExternalId: overrides.findByExternalId ?? (() => dies("findByExternalId")),
251
+ complete: overrides.complete ?? (() => dies("complete")),
252
+ completeRef: overrides.completeRef ?? (() => dies("completeRef")),
253
+ withCheckRun: overrides.withCheckRun ?? (() => dies("withCheckRun"))
253
254
  });
254
255
  /** {@link CheckRun.makeTest} behind a `Layer`. */
255
256
  static layerTest = (overrides = {}) => Layer.succeed(CheckRun, CheckRun.makeTest(overrides));
256
257
  };
257
- const unstubbed = (member) => {
258
- throw new Error(`CheckRun.makeTest: ${member}() was called but not stubbed — pass an override.`);
259
- };
258
+ const dies = unstubbed("CheckRun.makeTest");
260
259
  const wireOutput = (output) => {
261
260
  const capped = output.truncated();
262
261
  return {
package/CodeScanning.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { unstubbed } from "./internal/unstubbed.js";
1
2
  import { GitHubClient } from "./GitHubClient.js";
2
3
  import { Repo } from "./Repo.js";
3
4
  import { Context, Effect, Layer } from "effect";
@@ -25,17 +26,15 @@ var CodeScanning = class CodeScanning extends Context.Service()("@effected/githu
25
26
  * with a clean typecheck.
26
27
  */
27
28
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
28
- /** An in-memory double; unstubbed members die naming themselves. */
29
+ /** An in-memory double; an unstubbed member dies when run, naming itself. */
29
30
  static makeTest = (overrides = {}) => ({
30
- configure: overrides.configure ?? (() => unstubbed("configure")),
31
- languages: overrides.languages ?? (() => unstubbed("languages"))
31
+ configure: overrides.configure ?? (() => dies("configure")),
32
+ languages: overrides.languages ?? (() => dies("languages"))
32
33
  });
33
34
  /** {@link CodeScanning.makeTest} behind a `Layer`. */
34
35
  static layerTest = (overrides = {}) => Layer.succeed(CodeScanning, CodeScanning.makeTest(overrides));
35
36
  };
36
- const unstubbed = (member) => {
37
- throw new Error(`CodeScanning.makeTest: ${member}() was called but not stubbed — pass an override.`);
38
- };
37
+ const dies = unstubbed("CodeScanning.makeTest");
39
38
  const SETUP_KEYS = [
40
39
  "state",
41
40
  "languages",
@@ -1,3 +1,4 @@
1
+ import { unstubbed } from "./internal/unstubbed.js";
1
2
  import { GitHubClient } from "./GitHubClient.js";
2
3
  import { Repo } from "./Repo.js";
3
4
  import { Context, Effect, Layer } from "effect";
@@ -34,18 +35,16 @@ var DeploymentEnvironment = class DeploymentEnvironment extends Context.Service(
34
35
  * further down throws at import time with a clean typecheck.
35
36
  */
36
37
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
37
- /** An in-memory double; unstubbed members die naming themselves. */
38
+ /** An in-memory double; an unstubbed member dies when run, naming itself. */
38
39
  static makeTest = (overrides = {}) => ({
39
- upsert: overrides.upsert ?? (() => unstubbed("upsert")),
40
- list: overrides.list ?? (() => unstubbed("list")),
41
- delete: overrides.delete ?? (() => unstubbed("delete"))
40
+ upsert: overrides.upsert ?? (() => dies("upsert")),
41
+ list: overrides.list ?? (() => dies("list")),
42
+ delete: overrides.delete ?? (() => dies("delete"))
42
43
  });
43
44
  /** {@link DeploymentEnvironment.makeTest} behind a `Layer`. */
44
45
  static layerTest = (overrides = {}) => Layer.succeed(DeploymentEnvironment, DeploymentEnvironment.makeTest(overrides));
45
46
  };
46
- const unstubbed = (member) => {
47
- throw new Error(`DeploymentEnvironment.makeTest: ${member}() was called but not stubbed — pass an override.`);
48
- };
47
+ const dies = unstubbed("DeploymentEnvironment.makeTest");
49
48
  const make = (client) => {
50
49
  return {
51
50
  upsert: Effect.fn("DeploymentEnvironment.upsert")(function* (name, config = {}) {
package/GitBranch.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { GitHubError } from "./GitHubError.js";
2
2
  import { GraphQLDocument } from "./GraphQL.js";
3
+ import { unstubbed } from "./internal/unstubbed.js";
3
4
  import { GitHubClient } from "./GitHubClient.js";
4
5
  import { Repo } from "./Repo.js";
5
6
  import { Context, Effect, Layer, Option, Schema } from "effect";
@@ -50,23 +51,21 @@ var GitBranch = class GitBranch extends Context.Service()("@effected/github/GitB
50
51
  * layer is built.
51
52
  */
52
53
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
53
- /** An in-memory double; unstubbed members die naming themselves. */
54
+ /** An in-memory double; an unstubbed member dies when run, naming itself. */
54
55
  static makeTest = (overrides = {}) => ({
55
- create: overrides.create ?? (() => unstubbed("create")),
56
- upsert: overrides.upsert ?? (() => unstubbed("upsert")),
57
- exists: overrides.exists ?? (() => unstubbed("exists")),
58
- sha: overrides.sha ?? (() => unstubbed("sha")),
59
- shaOption: overrides.shaOption ?? (() => unstubbed("shaOption")),
60
- reset: overrides.reset ?? (() => unstubbed("reset")),
61
- delete: overrides.delete ?? (() => unstubbed("delete")),
62
- createLinked: overrides.createLinked ?? (() => unstubbed("createLinked"))
56
+ create: overrides.create ?? (() => dies("create")),
57
+ upsert: overrides.upsert ?? (() => dies("upsert")),
58
+ exists: overrides.exists ?? (() => dies("exists")),
59
+ sha: overrides.sha ?? (() => dies("sha")),
60
+ shaOption: overrides.shaOption ?? (() => dies("shaOption")),
61
+ reset: overrides.reset ?? (() => dies("reset")),
62
+ delete: overrides.delete ?? (() => dies("delete")),
63
+ createLinked: overrides.createLinked ?? (() => dies("createLinked"))
63
64
  });
64
65
  /** {@link GitBranch.makeTest} behind a `Layer`. */
65
66
  static layerTest = (overrides = {}) => Layer.succeed(GitBranch, GitBranch.makeTest(overrides));
66
67
  };
67
- const unstubbed = (member) => {
68
- throw new Error(`GitBranch.makeTest: ${member}() was called but not stubbed — pass an override.`);
69
- };
68
+ const dies = unstubbed("GitBranch.makeTest");
70
69
  /**
71
70
  * `main`, `refs/heads/main` and `heads/main` all name the same branch.
72
71
  *
package/GitCommit.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { unstubbed } from "./internal/unstubbed.js";
1
2
  import { GitHubClient } from "./GitHubClient.js";
2
3
  import { Repo } from "./Repo.js";
3
4
  import { Context, Effect, Layer, Schema } from "effect";
@@ -86,19 +87,17 @@ var CommitRef = class extends Schema.Class("CommitRef")({
86
87
  var GitCommit = class GitCommit extends Context.Service()("@effected/github/GitCommit") {
87
88
  /** The live service, built over a `GitHubClient`. */
88
89
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
89
- /** An in-memory double; unstubbed members die naming themselves. */
90
+ /** An in-memory double; an unstubbed member dies when run, naming itself. */
90
91
  static makeTest = (overrides = {}) => ({
91
- get: overrides.get ?? (() => unstubbed("get")),
92
- createTree: overrides.createTree ?? (() => unstubbed("createTree")),
93
- createCommit: overrides.createCommit ?? (() => unstubbed("createCommit")),
94
- commitFiles: overrides.commitFiles ?? (() => unstubbed("commitFiles"))
92
+ get: overrides.get ?? (() => dies("get")),
93
+ createTree: overrides.createTree ?? (() => dies("createTree")),
94
+ createCommit: overrides.createCommit ?? (() => dies("createCommit")),
95
+ commitFiles: overrides.commitFiles ?? (() => dies("commitFiles"))
95
96
  });
96
97
  /** {@link GitCommit.makeTest} behind a `Layer`. */
97
98
  static layerTest = (overrides = {}) => Layer.succeed(GitCommit, GitCommit.makeTest(overrides));
98
99
  };
99
- const unstubbed = (member) => {
100
- throw new Error(`GitCommit.makeTest: ${member}() was called but not stubbed — pass an override.`);
101
- };
100
+ const dies = unstubbed("GitCommit.makeTest");
102
101
  /** The Git Database tree entry for one change. */
103
102
  const treeEntry = (change) => change._tag === "FileContent" ? {
104
103
  path: change.path,
package/GitHubApp.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { GitHubError } from "./GitHubError.js";
2
2
  import { GitHubGraphQLError } from "./GraphQL.js";
3
- import { GitHubClient, makeClientShape } from "./GitHubClient.js";
3
+ import { unstubbed } from "./internal/unstubbed.js";
4
+ import { GitHubClient, makeClientShape, resolveApiVersion } from "./GitHubClient.js";
4
5
  import { numericId } from "./internal/ids.js";
5
6
  import { InstallationTokenStore } from "./InstallationTokenStore.js";
6
7
  import { Cause, Clock, Context, DateTime, Deferred, Duration, Effect, Layer, Option, Redacted, Ref, Schema, Semaphore, Stream } from "effect";
@@ -435,16 +436,16 @@ var GitHubApp = class GitHubApp extends Context.Service()("@effected/github/GitH
435
436
  ...options,
436
437
  token: cached.token.token
437
438
  })));
438
- /** An in-memory double; unstubbed members die naming themselves. */
439
+ /** An in-memory double; an unstubbed member dies when run, naming itself. */
439
440
  static makeTest = (overrides = {}) => {
440
- const token = overrides.token ?? (() => unstubbed("token"));
441
+ const token = overrides.token ?? (() => dies("token"));
441
442
  return {
442
443
  token,
443
- scopedToken: overrides.scopedToken ?? (() => unstubbed("scopedToken")),
444
- revoke: overrides.revoke ?? (() => unstubbed("revoke")),
445
- identity: overrides.identity ?? (() => unstubbed("identity")),
446
- botUser: overrides.botUser ?? (() => unstubbed("botUser")),
447
- installations: overrides.installations ?? (() => unstubbed("installations")),
444
+ scopedToken: overrides.scopedToken ?? (() => dies("scopedToken")),
445
+ revoke: overrides.revoke ?? (() => dies("revoke")),
446
+ identity: overrides.identity ?? (() => dies("identity")),
447
+ botUser: overrides.botUser ?? (() => dies("botUser")),
448
+ installations: overrides.installations ?? (() => dies("installations")),
448
449
  cachedToken: overrides.cachedToken ?? makeCachedToken(token)
449
450
  };
450
451
  };
@@ -515,9 +516,7 @@ const makeCachedToken = (token) => {
515
516
  };
516
517
  });
517
518
  };
518
- const unstubbed = (member) => {
519
- throw new Error(`GitHubApp.makeTest: ${member}() was called but not stubbed — pass an override.`);
520
- };
519
+ const dies = unstubbed("GitHubApp.makeTest");
521
520
  /** Mint an app JWT: iat 60 s in the past (clock drift), exp 9 minutes after now (GitHub caps at 10). */
522
521
  const mintJwt = (credentials) => Effect.gen(function* () {
523
522
  const key = yield* JwtKey.fromPkcs8Pem(credentials.privateKey, { alg: "RS256" });
@@ -545,7 +544,7 @@ const asBearer = (token, options) => makeClientShape({
545
544
  });
546
545
  const appFailure = (kind) => (error) => Effect.fail(GitHubAppError.of(kind, error.reason, error));
547
546
  function makeApp(options) {
548
- return Effect.sync(() => {
547
+ return Effect.map(resolveApiVersion(options.apiVersion), () => {
549
548
  const installations = Effect.fn("GitHubApp.installations")(function* (credentials) {
550
549
  return (yield* (yield* asApp(credentials, options)).paginate("GET /app/installations", {}).pipe(Effect.catch(appFailure("installation")))).map((entry) => {
551
550
  const account = entry.account ?? void 0;
@@ -635,6 +634,7 @@ const normalizePermissions = (raw) => {
635
634
  * release.
636
635
  */
637
636
  const makeRotatingClient = (mint, release, options, operation) => Effect.gen(function* () {
637
+ const apiVersion = yield* resolveApiVersion(options.apiVersion);
638
638
  const held = yield* Ref.make(Option.none());
639
639
  const revokeHeld = Effect.flatMap(Ref.get(held), (current) => Option.isSome(current) ? Effect.ignore(release(current.value.credential.token)) : Effect.void);
640
640
  const rotate = Effect.gen(function* () {
@@ -679,7 +679,8 @@ const makeRotatingClient = (mint, release, options, operation) => Effect.gen(fun
679
679
  paginate: (route, params, pageOptions) => Effect.flatMap(current, (client) => client.paginate(route, params, pageOptions)),
680
680
  paginateStream: (route, params, pageOptions) => Stream.unwrap(Effect.map(current, (client) => client.paginateStream(route, params, pageOptions))),
681
681
  graphql: (document, variables) => Effect.flatMap(currentForGraphQL, (client) => client.graphql(document, variables)),
682
- rateLimit: Effect.flatMap(Ref.get(held), (state) => Option.isSome(state) ? state.value.client.rateLimit : Effect.succeed(Option.none()))
682
+ rateLimit: Effect.flatMap(Ref.get(held), (state) => Option.isSome(state) ? state.value.client.rateLimit : Effect.succeed(Option.none())),
683
+ apiVersion
683
684
  };
684
685
  });
685
686
 
package/GitHubClient.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { GitHubError } from "./GitHubError.js";
2
2
  import { GitHubGraphQLError } from "./GraphQL.js";
3
3
  import { RetryPolicy } from "./Resilience.js";
4
- import { makeTransport } from "./internal/octokit.js";
4
+ import { DEFAULT_API_VERSION, makeTransport } from "./internal/octokit.js";
5
5
  import { fromArray, paginate } from "./internal/paginate.js";
6
+ import { unstubbed } from "./internal/unstubbed.js";
6
7
  import { Clock, Config, Context, Effect, Layer, Option, Schema, Stream } from "effect";
7
8
 
8
9
  // src/GitHubClient.ts
@@ -10,7 +11,7 @@ import { Clock, Config, Context, Effect, Layer, Option, Schema, Stream } from "e
10
11
  const DEFAULT_PER_PAGE = 100;
11
12
  /**
12
13
  * A raw HTTP failure recorded for {@link GitHubClient.layerFixture}, made by
13
- * {@link (GitHubFixtures:variable).failure}.
14
+ * {@link (GitHubFixtures:class).failure}.
14
15
  *
15
16
  * @remarks
16
17
  * Branded, so a fixture's data payload that happens to have a `status` and a
@@ -45,12 +46,42 @@ const isRawFailure = (value) => typeof value === "object" && value !== null && v
45
46
  *
46
47
  * @public
47
48
  */
48
- const GitHubFixtures = { failure: (response) => new RawFailure(response) };
49
+ var GitHubFixtures = class {
50
+ constructor() {}
51
+ /**
52
+ * A raw failure: what GitHub answered, classified at call time by the same
53
+ * classifier the live client uses.
54
+ *
55
+ * @remarks
56
+ * Record it wherever a fixture accepts a `GitHubError` (`request`, and so
57
+ * `requestDecoded`, and `paginate`). Each call fails with
58
+ * `GitHubError.fromResponse(route, response, now)`, `now` read from `Clock`,
59
+ * so a rate-limit reset header is measured against the `TestClock`. Prefer it
60
+ * to a hand-built `GitHubError` when the test is about GitHub's actual
61
+ * response: the classification is then the package's, not the test's.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * import { GitHubClient, GitHubFixtures } from "@effected/github";
66
+ *
67
+ * const layer = GitHubClient.layerFixture({
68
+ * request: {
69
+ * "POST /repos/{owner}/{repo}/releases": GitHubFixtures.failure({
70
+ * status: 422,
71
+ * body: {
72
+ * message: "Validation Failed",
73
+ * errors: [{ resource: "Release", code: "already_exists", field: "tag_name" }],
74
+ * },
75
+ * }),
76
+ * },
77
+ * });
78
+ * ```
79
+ */
80
+ static failure = (response) => new RawFailure(response);
81
+ };
49
82
  const resolvePolicy = (retry) => retry === void 0 ? RetryPolicy.default : retry === "off" ? RetryPolicy.none : retry;
50
83
  const perPageOf = (options) => options?.perPage ?? DEFAULT_PER_PAGE;
51
- const unstubbed = (member) => {
52
- throw new Error(`GitHubClient.makeTest: ${member}() was called but not stubbed — pass an override, or use GitHubClient.layerFixture for recorded responses.`);
53
- };
84
+ const dies = unstubbed("GitHubClient.makeTest", "pass an override, or use GitHubClient.layerFixture for recorded responses.");
54
85
  /**
55
86
  * The typed GitHub API client.
56
87
  *
@@ -118,21 +149,25 @@ var GitHubClient = class GitHubClient extends Context.Service()("@effected/githu
118
149
  * member **dies** naming itself.
119
150
  *
120
151
  * @remarks
121
- * No member has an honest default. A fabricated response — an empty list, a
152
+ * No request member has an honest default. A fabricated response — an empty list, a
122
153
  * made-up sha — would leak into the code under test as fact, so the double
123
154
  * fails loudly instead, which also makes it proof that a test touches nothing
124
155
  * but what it stubbed.
125
156
  *
157
+ * `rateLimit` answers `Option.none()` and `apiVersion` the live default,
158
+ * because a real client legitimately answers both without a request.
159
+ *
126
160
  * For recorded responses that page for real, use
127
161
  * {@link GitHubClient.layerFixture}.
128
162
  */
129
163
  static makeTest = (overrides = {}) => ({
130
- request: overrides.request ?? (() => unstubbed("request")),
131
- requestDecoded: overrides.requestDecoded ?? (() => unstubbed("requestDecoded")),
132
- paginate: overrides.paginate ?? (() => unstubbed("paginate")),
133
- paginateStream: overrides.paginateStream ?? (() => unstubbed("paginateStream")),
134
- graphql: overrides.graphql ?? (() => unstubbed("graphql")),
135
- rateLimit: overrides.rateLimit ?? Effect.succeed(Option.none())
164
+ request: overrides.request ?? (() => dies("request")),
165
+ requestDecoded: overrides.requestDecoded ?? (() => dies("requestDecoded")),
166
+ paginate: overrides.paginate ?? (() => dies("paginate")),
167
+ paginateStream: overrides.paginateStream ?? (() => Stream.fromEffect(dies("paginateStream"))),
168
+ graphql: overrides.graphql ?? (() => dies("graphql")),
169
+ rateLimit: overrides.rateLimit ?? Effect.succeed(Option.none()),
170
+ apiVersion: overrides.apiVersion ?? "2026-03-10"
136
171
  });
137
172
  /** {@link GitHubClient.makeTest} behind a `Layer`. */
138
173
  static layerTest = (overrides = {}) => Layer.succeed(GitHubClient, GitHubClient.makeTest(overrides));
@@ -150,13 +185,28 @@ var GitHubClient = class GitHubClient extends Context.Service()("@effected/githu
150
185
  *
151
186
  * **It records every call it serves, including one that fails.** A call
152
187
  * answered by a recorded `GitHubError` or a
153
- * {@link (GitHubFixtures:variable).failure} is recorded before it fails, so
188
+ * {@link (GitHubFixtures:class).failure} is recorded before it fails, so
154
189
  * one fixture both records the calls and classifies a raw failure the way
155
190
  * the live client would; there is no need to classify by hand inside a
156
191
  * {@link GitHubClient.layerTest} handler.
157
192
  */
158
193
  static layerFixture = (fixtures) => Layer.succeed(GitHubClient, makeFixture(fixtures));
159
194
  };
195
+ /** A calendar api-version: `YYYY-MM-DD`. */
196
+ const API_VERSION_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
197
+ /**
198
+ * The api-version a client pins: the option when it is a calendar version,
199
+ * the default when it is absent, and a defect naming the option otherwise.
200
+ *
201
+ * @remarks
202
+ * A defect, not a typed failure: a malformed version is a wiring mistake no
203
+ * caller recovers from, and every layer that builds a client would otherwise
204
+ * widen its error channel for it. Exported for `GitHubApp`, which validates
205
+ * eagerly at its own layer construction. Not part of the public surface.
206
+ *
207
+ * @internal
208
+ */
209
+ const resolveApiVersion = (option) => option === void 0 ? Effect.succeed(DEFAULT_API_VERSION) : API_VERSION_PATTERN.test(option) ? Effect.succeed(option) : Effect.die(/* @__PURE__ */ new Error(`GitHubClient: the apiVersion option must be a calendar version like "${DEFAULT_API_VERSION}" (YYYY-MM-DD), got ${JSON.stringify(option)}`));
160
210
  /**
161
211
  * Builds the live shape over a constructed transport.
162
212
  *
@@ -168,12 +218,14 @@ var GitHubClient = class GitHubClient extends Context.Service()("@effected/githu
168
218
  * @internal
169
219
  */
170
220
  const makeClientShape = (options) => Effect.gen(function* () {
221
+ const apiVersion = yield* resolveApiVersion(options.apiVersion);
171
222
  const transport = yield* makeTransport({
172
223
  token: options.token,
173
224
  retry: resolvePolicy(options.retry),
174
225
  baseUrl: options.baseUrl,
175
226
  userAgent: options.userAgent,
176
- fetch: options.fetch
227
+ fetch: options.fetch,
228
+ apiVersion
177
229
  });
178
230
  const request = Effect.fn("GitHubClient.request")(function* (route, params) {
179
231
  yield* Effect.annotateCurrentSpan({ route });
@@ -205,7 +257,8 @@ const makeClientShape = (options) => Effect.gen(function* () {
205
257
  const raw = yield* transport.graphql(document.name, document.document, document.encodeVariables(variables));
206
258
  return yield* document.decode(raw).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubGraphQLError.decode(document.name, "response did not match its schema", error))));
207
259
  }),
208
- rateLimit: transport.rateLimit
260
+ rateLimit: transport.rateLimit,
261
+ apiVersion
209
262
  };
210
263
  });
211
264
  const makeFixture = (fixtures) => {
@@ -272,8 +325,9 @@ const makeFixture = (fixtures) => {
272
325
  const raw = fixtures.graphql?.[document.name];
273
326
  return raw === void 0 ? Effect.die(/* @__PURE__ */ new Error(`GitHubClient.layerFixture: no graphql fixture for ${document.name}`)) : document.decode(raw).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubGraphQLError.decode(document.name, "fixture did not match its schema", error))));
274
327
  },
275
- rateLimit: Effect.succeed(Option.fromUndefinedOr(fixtures.rateLimit))
328
+ rateLimit: Effect.succeed(Option.fromUndefinedOr(fixtures.rateLimit)),
329
+ apiVersion: fixtures.apiVersion ?? "2026-03-10"
276
330
  };
277
331
  };
278
332
 
279
- export { GitHubClient, GitHubFixtures, RawFailure, makeClientShape };
333
+ export { GitHubClient, GitHubFixtures, RawFailure, makeClientShape, resolveApiVersion };
package/GitHubCommit.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { paginate } from "./internal/paginate.js";
2
+ import { unstubbed } from "./internal/unstubbed.js";
2
3
  import { GitHubClient } from "./GitHubClient.js";
3
4
  import { Repo } from "./Repo.js";
4
5
  import { Context, Effect, Layer, Option, Schema, Stream } from "effect";
@@ -114,19 +115,17 @@ var CommitComparison = class extends Schema.Class("CommitComparison")({
114
115
  var GitHubCommit = class GitHubCommit extends Context.Service()("@effected/github/GitHubCommit") {
115
116
  /** The live service, built over a `GitHubClient`. */
116
117
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
117
- /** An in-memory double; unstubbed members die naming themselves. */
118
+ /** An in-memory double; an unstubbed member dies when run, naming itself. */
118
119
  static makeTest = (overrides = {}) => ({
119
- get: overrides.get ?? (() => unstubbed("get")),
120
- list: overrides.list ?? (() => unstubbed("list")),
121
- compare: overrides.compare ?? (() => unstubbed("compare")),
122
- changedFiles: overrides.changedFiles ?? (() => unstubbed("changedFiles"))
120
+ get: overrides.get ?? (() => dies("get")),
121
+ list: overrides.list ?? (() => dies("list")),
122
+ compare: overrides.compare ?? (() => dies("compare")),
123
+ changedFiles: overrides.changedFiles ?? (() => dies("changedFiles"))
123
124
  });
124
125
  /** {@link GitHubCommit.makeTest} behind a `Layer`. */
125
126
  static layerTest = (overrides = {}) => Layer.succeed(GitHubCommit, GitHubCommit.makeTest(overrides));
126
127
  };
127
- const unstubbed = (member) => {
128
- throw new Error(`GitHubCommit.makeTest: ${member}() was called but not stubbed — pass an override.`);
129
- };
128
+ const dies = unstubbed("GitHubCommit.makeTest");
130
129
  const summarize = (raw) => CommitSummary.make({
131
130
  sha: raw.sha,
132
131
  message: raw.commit.message,
package/GitHubContent.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { GitHubError } from "./GitHubError.js";
2
+ import { unstubbed } from "./internal/unstubbed.js";
2
3
  import { GitHubClient } from "./GitHubClient.js";
3
4
  import { Repo } from "./Repo.js";
4
5
  import { Context, Effect, Layer, Option, Result } from "effect";
@@ -35,17 +36,15 @@ const utf8 = new TextDecoder("utf-8", { ignoreBOM: true });
35
36
  var GitHubContent = class GitHubContent extends Context.Service()("@effected/github/GitHubContent") {
36
37
  /** The live service, built over a `GitHubClient`. */
37
38
  static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
38
- /** An in-memory double; unstubbed members die naming themselves. */
39
+ /** An in-memory double; an unstubbed member dies when run, naming itself. */
39
40
  static makeTest = (overrides = {}) => ({
40
- getFile: overrides.getFile ?? (() => unstubbed("getFile")),
41
- getFileOption: overrides.getFileOption ?? (() => unstubbed("getFileOption"))
41
+ getFile: overrides.getFile ?? (() => dies("getFile")),
42
+ getFileOption: overrides.getFileOption ?? (() => dies("getFileOption"))
42
43
  });
43
44
  /** {@link GitHubContent.makeTest} behind a `Layer`. */
44
45
  static layerTest = (overrides = {}) => Layer.succeed(GitHubContent, GitHubContent.makeTest(overrides));
45
46
  };
46
- const unstubbed = (member) => {
47
- throw new Error(`GitHubContent.makeTest: ${member}() was called but not stubbed — pass an override.`);
48
- };
47
+ const dies = unstubbed("GitHubContent.makeTest");
49
48
  const make = (client) => {
50
49
  const getFile = Effect.fn("GitHubContent.getFile")(function* (path, options) {
51
50
  const { owner, repo } = yield* Repo;