@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 +17 -4
- package/ArtifactMetadata.js +4 -5
- package/Attestation.js +9 -6
- package/AuthenticatedApp.js +8 -9
- package/CheckRun.js +12 -13
- package/CodeScanning.js +5 -6
- package/DeploymentEnvironment.js +6 -7
- package/GitBranch.js +11 -12
- package/GitCommit.js +7 -8
- package/GitHubApp.js +14 -13
- package/GitHubClient.js +72 -18
- package/GitHubCommit.js +7 -8
- package/GitHubContent.js +5 -6
- package/GitHubIssue.js +16 -46
- package/GitHubRelease.js +10 -11
- package/GitHubRepository.js +63 -24
- package/GitTag.js +9 -10
- package/InstallationTokenStore.js +5 -3
- package/PullRequest.js +112 -37
- package/PullRequestComment.js +7 -8
- package/README.md +8 -4
- package/RepositorySecret.js +9 -10
- package/RepositorySecurity.js +9 -10
- package/RepositoryVariable.js +9 -10
- package/Ruleset.js +8 -9
- package/WorkflowDispatch.js +13 -11
- package/index.d.ts +231 -78
- package/internal/octokit.js +16 -8
- package/internal/repositoryRestSettingKeys.js +44 -0
- package/internal/unstubbed.js +25 -0
- package/package.json +2 -2
package/GitHubIssue.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GraphQLDocument } from "./GraphQL.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";
|
|
@@ -127,45 +128,20 @@ const CrossReferencedDocument = GraphQLDocument.make({
|
|
|
127
128
|
var GitHubIssue = class GitHubIssue extends Context.Service()("@effected/github/GitHubIssue") {
|
|
128
129
|
/** The live service, built over a `GitHubClient`. */
|
|
129
130
|
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
130
|
-
/** An in-memory double; unstubbed
|
|
131
|
+
/** An in-memory double; an unstubbed member dies when run, naming itself. */
|
|
131
132
|
static makeTest = (overrides = {}) => ({
|
|
132
|
-
get: overrides.get ?? (() =>
|
|
133
|
-
list: overrides.list ?? (() =>
|
|
134
|
-
close: overrides.close ?? (() =>
|
|
135
|
-
comment: overrides.comment ?? (() =>
|
|
136
|
-
commentOnce: overrides.commentOnce ?? (() =>
|
|
137
|
-
linkedIssues: overrides.linkedIssues ?? (() =>
|
|
138
|
-
isCrossReferencedBy: overrides.isCrossReferencedBy ?? (() =>
|
|
133
|
+
get: overrides.get ?? (() => dies("get")),
|
|
134
|
+
list: overrides.list ?? (() => dies("list")),
|
|
135
|
+
close: overrides.close ?? (() => dies("close")),
|
|
136
|
+
comment: overrides.comment ?? (() => dies("comment")),
|
|
137
|
+
commentOnce: overrides.commentOnce ?? (() => dies("commentOnce")),
|
|
138
|
+
linkedIssues: overrides.linkedIssues ?? (() => dies("linkedIssues")),
|
|
139
|
+
isCrossReferencedBy: overrides.isCrossReferencedBy ?? (() => dies("isCrossReferencedBy"))
|
|
139
140
|
});
|
|
140
141
|
/** {@link GitHubIssue.makeTest} behind a `Layer`. */
|
|
141
142
|
static layerTest = (overrides = {}) => Layer.succeed(GitHubIssue, GitHubIssue.makeTest(overrides));
|
|
142
143
|
};
|
|
143
|
-
const
|
|
144
|
-
throw new Error(`GitHubIssue.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
145
|
-
};
|
|
146
|
-
/**
|
|
147
|
-
* The REST calendar version this module's requests pin.
|
|
148
|
-
*
|
|
149
|
-
* @remarks
|
|
150
|
-
* The routes are not deprecated — the **default api-version** is. When no
|
|
151
|
-
* `x-github-api-version` header is sent (octokit sends none), GitHub serves
|
|
152
|
-
* calendar version 2022-11-28, which it deprecated when 2026-03-10 shipped and
|
|
153
|
-
* sunsets on 2028-03-10. Every response from an endpoint the new version
|
|
154
|
-
* changed then carries a `Deprecation` header, which `@octokit/request` prints
|
|
155
|
-
* straight to the consumer's console — the constructor's `log` option cannot
|
|
156
|
-
* silence it, because the fetch wrapper resolves `request.log ?? console`.
|
|
157
|
-
* `PATCH /repos/{owner}/{repo}/issues/{issue_number}` is on that list (the
|
|
158
|
-
* singular `assignee` parameter was removed), so every `close` warned once per
|
|
159
|
-
* issue in consumer workflow logs.
|
|
160
|
-
*
|
|
161
|
-
* Nothing this module sends or reads changed in 2026-03-10: `state` and
|
|
162
|
-
* `state_reason` are untouched, and the projections read only fields both
|
|
163
|
-
* versions carry. Pinning the current version is therefore the whole fix, and
|
|
164
|
-
* it deliberately does **not** hide future deprecations — when GitHub
|
|
165
|
-
* deprecates 2026-03-10 in turn, the header comes back and octokit warns
|
|
166
|
-
* again.
|
|
167
|
-
*/
|
|
168
|
-
const API_VERSION_HEADERS = { "x-github-api-version": "2026-03-10" };
|
|
144
|
+
const dies = unstubbed("GitHubIssue.makeTest");
|
|
169
145
|
/** GitHub sends a label as either a string or an object; callers want the name. */
|
|
170
146
|
const labelNames = (labels) => labels.flatMap((label) => typeof label === "string" ? [label] : label.name !== void 0 ? [label.name] : []);
|
|
171
147
|
const project = (raw) => IssueInfo.make({
|
|
@@ -187,8 +163,7 @@ const make = (client) => ({
|
|
|
187
163
|
const raw = yield* client.request("GET /repos/{owner}/{repo}/issues/{issue_number}", {
|
|
188
164
|
owner,
|
|
189
165
|
repo,
|
|
190
|
-
issue_number: number
|
|
191
|
-
headers: API_VERSION_HEADERS
|
|
166
|
+
issue_number: number
|
|
192
167
|
});
|
|
193
168
|
return project(raw);
|
|
194
169
|
}),
|
|
@@ -202,8 +177,7 @@ const make = (client) => ({
|
|
|
202
177
|
owner,
|
|
203
178
|
repo,
|
|
204
179
|
...options?.state !== void 0 ? { state: options.state } : {},
|
|
205
|
-
...options?.labels !== void 0 ? { labels: options.labels.join(",") } : {}
|
|
206
|
-
headers: API_VERSION_HEADERS
|
|
180
|
+
...options?.labels !== void 0 ? { labels: options.labels.join(",") } : {}
|
|
207
181
|
}, options?.page)).map(project);
|
|
208
182
|
}),
|
|
209
183
|
close: Effect.fn("GitHubIssue.close")(function* (number, reason) {
|
|
@@ -218,8 +192,7 @@ const make = (client) => ({
|
|
|
218
192
|
repo,
|
|
219
193
|
issue_number: number,
|
|
220
194
|
state: "closed",
|
|
221
|
-
...reason !== void 0 ? { state_reason: reason } : {}
|
|
222
|
-
headers: API_VERSION_HEADERS
|
|
195
|
+
...reason !== void 0 ? { state_reason: reason } : {}
|
|
223
196
|
});
|
|
224
197
|
}),
|
|
225
198
|
comment: Effect.fn("GitHubIssue.comment")(function* (number, body) {
|
|
@@ -233,8 +206,7 @@ const make = (client) => ({
|
|
|
233
206
|
owner,
|
|
234
207
|
repo,
|
|
235
208
|
issue_number: number,
|
|
236
|
-
body
|
|
237
|
-
headers: API_VERSION_HEADERS
|
|
209
|
+
body
|
|
238
210
|
});
|
|
239
211
|
return numericId(created.id);
|
|
240
212
|
}),
|
|
@@ -249,8 +221,7 @@ const make = (client) => ({
|
|
|
249
221
|
const existing = (yield* client.paginate("GET /repos/{owner}/{repo}/issues/{issue_number}/comments", {
|
|
250
222
|
owner,
|
|
251
223
|
repo,
|
|
252
|
-
issue_number: issueNumber
|
|
253
|
-
headers: API_VERSION_HEADERS
|
|
224
|
+
issue_number: issueNumber
|
|
254
225
|
})).find((comment) => marker.matches(comment.body ?? ""));
|
|
255
226
|
if (existing !== void 0) return CommentOnceResult.make({
|
|
256
227
|
wrote: false,
|
|
@@ -264,8 +235,7 @@ const make = (client) => ({
|
|
|
264
235
|
owner,
|
|
265
236
|
repo,
|
|
266
237
|
issue_number: issueNumber,
|
|
267
|
-
body: `${body}\n\n${marker.html}
|
|
268
|
-
headers: API_VERSION_HEADERS
|
|
238
|
+
body: `${body}\n\n${marker.html}`
|
|
269
239
|
});
|
|
270
240
|
return CommentOnceResult.make({
|
|
271
241
|
wrote: true,
|
package/GitHubRelease.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, Schema } from "effect";
|
|
@@ -59,22 +60,20 @@ var ReleaseAsset = class extends Schema.Class("ReleaseAsset")({
|
|
|
59
60
|
var GitHubRelease = class GitHubRelease extends Context.Service()("@effected/github/GitHubRelease") {
|
|
60
61
|
/** The live service, built over a `GitHubClient`. */
|
|
61
62
|
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
62
|
-
/** An in-memory double; unstubbed
|
|
63
|
+
/** An in-memory double; an unstubbed member dies when run, naming itself. */
|
|
63
64
|
static makeTest = (overrides = {}) => ({
|
|
64
|
-
create: overrides.create ?? (() =>
|
|
65
|
-
getByTag: overrides.getByTag ?? (() =>
|
|
66
|
-
getByTagOption: overrides.getByTagOption ?? (() =>
|
|
67
|
-
list: overrides.list ?? (() =>
|
|
68
|
-
update: overrides.update ?? (() =>
|
|
69
|
-
uploadAsset: overrides.uploadAsset ?? (() =>
|
|
70
|
-
listAssets: overrides.listAssets ?? (() =>
|
|
65
|
+
create: overrides.create ?? (() => dies("create")),
|
|
66
|
+
getByTag: overrides.getByTag ?? (() => dies("getByTag")),
|
|
67
|
+
getByTagOption: overrides.getByTagOption ?? (() => dies("getByTagOption")),
|
|
68
|
+
list: overrides.list ?? (() => dies("list")),
|
|
69
|
+
update: overrides.update ?? (() => dies("update")),
|
|
70
|
+
uploadAsset: overrides.uploadAsset ?? (() => dies("uploadAsset")),
|
|
71
|
+
listAssets: overrides.listAssets ?? (() => dies("listAssets"))
|
|
71
72
|
});
|
|
72
73
|
/** {@link GitHubRelease.makeTest} behind a `Layer`. */
|
|
73
74
|
static layerTest = (overrides = {}) => Layer.succeed(GitHubRelease, GitHubRelease.makeTest(overrides));
|
|
74
75
|
};
|
|
75
|
-
const
|
|
76
|
-
throw new Error(`GitHubRelease.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
77
|
-
};
|
|
76
|
+
const dies = unstubbed("GitHubRelease.makeTest");
|
|
78
77
|
const project = (raw) => ReleaseInfo.make({
|
|
79
78
|
id: raw.id,
|
|
80
79
|
tag: raw.tag_name,
|
package/GitHubRepository.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { GraphQLDocument } from "./GraphQL.js";
|
|
2
|
+
import { unstubbed, unstubbedRead } from "./internal/unstubbed.js";
|
|
2
3
|
import { GitHubClient } from "./GitHubClient.js";
|
|
3
4
|
import { Repo } from "./Repo.js";
|
|
5
|
+
import { REMOVED_BY_PINNED_VERSION, REPOSITORY_REST_SETTING_KEYS } from "./internal/repositoryRestSettingKeys.js";
|
|
4
6
|
import { Context, Effect, Layer, Schema } from "effect";
|
|
5
7
|
|
|
6
8
|
// src/GitHubRepository.ts
|
|
@@ -63,17 +65,21 @@ const SECURITY_ANALYSIS_STATUS_FIELDS = /* @__PURE__ */ new Set([
|
|
|
63
65
|
"dependabot_security_updates"
|
|
64
66
|
]);
|
|
65
67
|
/**
|
|
66
|
-
* Settings
|
|
67
|
-
*
|
|
68
|
+
* Settings written through the GraphQL `updateRepository` mutation, mapped
|
|
69
|
+
* from snake_case keys to camelCase GraphQL input fields.
|
|
68
70
|
*
|
|
69
71
|
* @remarks
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* `
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
72
|
+
* `applySettings` checks this map **first**, so a key here always goes
|
|
73
|
+
* through GraphQL, even when the REST patch also declares it.
|
|
74
|
+
* `has_sponsorships` and `has_discussions` are GraphQL-only: the
|
|
75
|
+
* `PATCH /repos/{owner}/{repo}` route accepts neither, and `has_discussions`
|
|
76
|
+
* is the treacherous one — the REST **read** returns it, so routing its write
|
|
77
|
+
* to the PATCH looks symmetric, but the PATCH silently ignores unknown body
|
|
78
|
+
* fields and answers 200, so the write would look applied while the
|
|
79
|
+
* repository never changed. `has_pull_requests` is in the REST patch's body
|
|
80
|
+
* too; GraphQL is preferred for it so the three toggles travel together.
|
|
81
|
+
* Setting any of these forces a second round trip to learn the repository's
|
|
82
|
+
* node id.
|
|
77
83
|
*
|
|
78
84
|
* @public
|
|
79
85
|
*/
|
|
@@ -170,12 +176,13 @@ const preparePatch = (patch) => {
|
|
|
170
176
|
};
|
|
171
177
|
/**
|
|
172
178
|
* Read and update a repository's settings, including settings only GraphQL can
|
|
173
|
-
* write, and look up its default branch, node id
|
|
179
|
+
* write, read and replace its topics, and look up its default branch, node id
|
|
180
|
+
* and owner type.
|
|
174
181
|
*
|
|
175
182
|
* @remarks
|
|
176
183
|
* Provide it with {@link GitHubRepository.layer}, which needs a `GitHubClient`;
|
|
177
|
-
* every member also needs a `Repo` in `R`. `settings`, `defaultBranch`, `nodeId
|
|
178
|
-
* and `
|
|
184
|
+
* every member also needs a `Repo` in `R`. `settings`, `defaultBranch`, `nodeId`,
|
|
185
|
+
* `ownerType` and `topics` are `Effect` values, not functions.
|
|
179
186
|
*
|
|
180
187
|
* @example
|
|
181
188
|
* ```ts
|
|
@@ -186,7 +193,7 @@ const preparePatch = (patch) => {
|
|
|
186
193
|
* const repository = yield* GitHubRepository;
|
|
187
194
|
* const branch = yield* repository.defaultBranch;
|
|
188
195
|
* const applied = yield* repository.applySettings({ has_wiki: false, has_discussions: true });
|
|
189
|
-
* return { branch, applied }; // applied.rest, applied.graphql
|
|
196
|
+
* return { branch, applied }; // applied.rest, applied.graphql, applied.ignored
|
|
190
197
|
* });
|
|
191
198
|
* ```
|
|
192
199
|
*
|
|
@@ -195,21 +202,22 @@ const preparePatch = (patch) => {
|
|
|
195
202
|
var GitHubRepository = class GitHubRepository extends Context.Service()("@effected/github/GitHubRepository") {
|
|
196
203
|
/** The live service, built over a `GitHubClient`. */
|
|
197
204
|
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
198
|
-
/** An in-memory double; unstubbed
|
|
205
|
+
/** An in-memory double; an unstubbed member dies when run, naming itself. */
|
|
199
206
|
static makeTest = (overrides = {}) => ({
|
|
200
|
-
settings: overrides.settings ??
|
|
201
|
-
updateSettings: overrides.updateSettings ?? (() =>
|
|
202
|
-
defaultBranch: overrides.defaultBranch ??
|
|
203
|
-
nodeId: overrides.nodeId ??
|
|
204
|
-
ownerType: overrides.ownerType ??
|
|
205
|
-
applySettings: overrides.applySettings ?? (() =>
|
|
207
|
+
settings: overrides.settings ?? unread("settings"),
|
|
208
|
+
updateSettings: overrides.updateSettings ?? (() => dies("updateSettings")),
|
|
209
|
+
defaultBranch: overrides.defaultBranch ?? unread("defaultBranch"),
|
|
210
|
+
nodeId: overrides.nodeId ?? unread("nodeId"),
|
|
211
|
+
ownerType: overrides.ownerType ?? unread("ownerType"),
|
|
212
|
+
applySettings: overrides.applySettings ?? (() => dies("applySettings")),
|
|
213
|
+
topics: overrides.topics ?? unread("topics"),
|
|
214
|
+
setTopics: overrides.setTopics ?? (() => dies("setTopics"))
|
|
206
215
|
});
|
|
207
216
|
/** {@link GitHubRepository.makeTest} behind a `Layer`. */
|
|
208
217
|
static layerTest = (overrides = {}) => Layer.succeed(GitHubRepository, GitHubRepository.makeTest(overrides));
|
|
209
218
|
};
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
};
|
|
219
|
+
const dies = unstubbed("GitHubRepository.makeTest");
|
|
220
|
+
const unread = unstubbedRead("GitHubRepository.makeTest");
|
|
213
221
|
const make = (client) => {
|
|
214
222
|
const settings = Effect.fn("GitHubRepository.settings")(function* () {
|
|
215
223
|
const { owner, repo } = yield* Repo;
|
|
@@ -229,6 +237,30 @@ const make = (client) => {
|
|
|
229
237
|
})();
|
|
230
238
|
return {
|
|
231
239
|
settings,
|
|
240
|
+
topics: Effect.fn("GitHubRepository.topics")(function* () {
|
|
241
|
+
const { owner, repo } = yield* Repo;
|
|
242
|
+
yield* Effect.annotateCurrentSpan({
|
|
243
|
+
owner,
|
|
244
|
+
repo
|
|
245
|
+
});
|
|
246
|
+
return (yield* client.request("GET /repos/{owner}/{repo}/topics", {
|
|
247
|
+
owner,
|
|
248
|
+
repo
|
|
249
|
+
})).names;
|
|
250
|
+
})(),
|
|
251
|
+
setTopics: Effect.fn("GitHubRepository.setTopics")(function* (names) {
|
|
252
|
+
const { owner, repo } = yield* Repo;
|
|
253
|
+
yield* Effect.annotateCurrentSpan({
|
|
254
|
+
owner,
|
|
255
|
+
repo,
|
|
256
|
+
topics: names.length
|
|
257
|
+
});
|
|
258
|
+
return (yield* client.request("PUT /repos/{owner}/{repo}/topics", {
|
|
259
|
+
owner,
|
|
260
|
+
repo,
|
|
261
|
+
names: [...names]
|
|
262
|
+
})).names;
|
|
263
|
+
}),
|
|
232
264
|
updateSettings: Effect.fn("GitHubRepository.updateSettings")(function* (patch) {
|
|
233
265
|
const { owner, repo } = yield* Repo;
|
|
234
266
|
yield* Effect.annotateCurrentSpan({
|
|
@@ -254,6 +286,7 @@ const make = (client) => {
|
|
|
254
286
|
const rest = {};
|
|
255
287
|
const graphql = {};
|
|
256
288
|
const graphqlKeys = [];
|
|
289
|
+
const ignored = [];
|
|
257
290
|
for (const [key, value] of Object.entries(input)) {
|
|
258
291
|
const graphqlField = Object.hasOwn(GRAPHQL_ONLY_SETTINGS, key) ? GRAPHQL_ONLY_SETTINGS[key] : void 0;
|
|
259
292
|
if (graphqlField !== void 0) {
|
|
@@ -261,6 +294,11 @@ const make = (client) => {
|
|
|
261
294
|
graphqlKeys.push(key);
|
|
262
295
|
continue;
|
|
263
296
|
}
|
|
297
|
+
const removedIn = Object.hasOwn(REMOVED_BY_PINNED_VERSION, key) ? REMOVED_BY_PINNED_VERSION[key] : void 0;
|
|
298
|
+
if (!REPOSITORY_REST_SETTING_KEYS.has(key) || removedIn !== void 0 && client.apiVersion >= removedIn) {
|
|
299
|
+
ignored.push(key);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
264
302
|
rest[key] = value;
|
|
265
303
|
}
|
|
266
304
|
const prepared = preparePatch(rest);
|
|
@@ -282,7 +320,8 @@ const make = (client) => {
|
|
|
282
320
|
}
|
|
283
321
|
return {
|
|
284
322
|
rest: restKeys,
|
|
285
|
-
graphql: graphqlKeys
|
|
323
|
+
graphql: graphqlKeys,
|
|
324
|
+
ignored
|
|
286
325
|
};
|
|
287
326
|
})
|
|
288
327
|
};
|
package/GitTag.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, Schema, Stream } from "effect";
|
|
@@ -63,21 +64,19 @@ const versionFromTag = (tag) => {
|
|
|
63
64
|
var GitTag = class GitTag extends Context.Service()("@effected/github/GitTag") {
|
|
64
65
|
/** The live service, built over a `GitHubClient`. */
|
|
65
66
|
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
66
|
-
/** An in-memory double; unstubbed
|
|
67
|
+
/** An in-memory double; an unstubbed member dies when run, naming itself. */
|
|
67
68
|
static makeTest = (overrides = {}) => ({
|
|
68
|
-
create: overrides.create ?? (() =>
|
|
69
|
-
upsert: overrides.upsert ?? (() =>
|
|
70
|
-
delete: overrides.delete ?? (() =>
|
|
71
|
-
list: overrides.list ?? (() =>
|
|
72
|
-
resolve: overrides.resolve ?? (() =>
|
|
73
|
-
latestSemver: overrides.latestSemver ?? (() =>
|
|
69
|
+
create: overrides.create ?? (() => dies("create")),
|
|
70
|
+
upsert: overrides.upsert ?? (() => dies("upsert")),
|
|
71
|
+
delete: overrides.delete ?? (() => dies("delete")),
|
|
72
|
+
list: overrides.list ?? (() => dies("list")),
|
|
73
|
+
resolve: overrides.resolve ?? (() => dies("resolve")),
|
|
74
|
+
latestSemver: overrides.latestSemver ?? (() => dies("latestSemver"))
|
|
74
75
|
});
|
|
75
76
|
/** {@link GitTag.makeTest} behind a `Layer`. */
|
|
76
77
|
static layerTest = (overrides = {}) => Layer.succeed(GitTag, GitTag.makeTest(overrides));
|
|
77
78
|
};
|
|
78
|
-
const
|
|
79
|
-
throw new Error(`GitTag.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
80
|
-
};
|
|
79
|
+
const dies = unstubbed("GitTag.makeTest");
|
|
81
80
|
const shortTag = (tag) => tag.replace(/^refs\/tags\//, "").replace(/^tags\//, "");
|
|
82
81
|
const rejectEmpty = (operation, tag) => {
|
|
83
82
|
const short = shortTag(tag).trim();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
1
2
|
import { Clock, Context, Duration, Effect, Layer, Option, Ref } from "effect";
|
|
2
3
|
|
|
3
4
|
// src/InstallationTokenStore.ts
|
|
@@ -38,13 +39,14 @@ var InstallationTokenStore = class InstallationTokenStore extends Context.Servic
|
|
|
38
39
|
}));
|
|
39
40
|
})
|
|
40
41
|
})));
|
|
41
|
-
/** An in-memory double; unstubbed
|
|
42
|
+
/** An in-memory double; an unstubbed member dies when run, naming itself. */
|
|
42
43
|
static makeTest = (overrides = {}) => ({
|
|
43
|
-
get: overrides.get ?? (() =>
|
|
44
|
-
set: overrides.set ?? (() =>
|
|
44
|
+
get: overrides.get ?? (() => dies("get")),
|
|
45
|
+
set: overrides.set ?? (() => dies("set"))
|
|
45
46
|
});
|
|
46
47
|
/** {@link InstallationTokenStore.makeTest} behind a `Layer`. */
|
|
47
48
|
static layerTest = (overrides = {}) => Layer.succeed(InstallationTokenStore, InstallationTokenStore.makeTest(overrides));
|
|
48
49
|
};
|
|
50
|
+
const dies = unstubbed("InstallationTokenStore.makeTest");
|
|
49
51
|
|
|
50
52
|
export { InstallationTokenStore };
|
package/PullRequest.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GitHubError } from "./GitHubError.js";
|
|
2
|
-
import { GraphQLDocument } from "./GraphQL.js";
|
|
2
|
+
import { GitHubGraphQLError, 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 { fileOf } from "./GitHubCommit.js";
|
|
@@ -54,7 +55,27 @@ var PullRequestInfo = class extends Schema.Class("PullRequestInfo")({
|
|
|
54
55
|
mergedAt: Schema.Option(Schema.DateTimeUtcFromString),
|
|
55
56
|
/** The description, when GitHub sent one. */
|
|
56
57
|
body: Schema.optionalKey(Schema.String),
|
|
57
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* The sha of the commit the merge produced; absent until the pull request
|
|
60
|
+
* has merged.
|
|
61
|
+
*
|
|
62
|
+
* @remarks
|
|
63
|
+
* Read from GraphQL's `PullRequest.mergeCommit.oid`. The REST pull request
|
|
64
|
+
* object no longer carries `merge_commit_sha` under the api-version this
|
|
65
|
+
* package pins, so a merged pull request costs one extra GraphQL round trip
|
|
66
|
+
* per single read (`get`, `update`, `upsert`), and an open or
|
|
67
|
+
* closed-unmerged one costs nothing. When the REST response does carry the
|
|
68
|
+
* field (a client pinned to an older api-version), it is used for a merged
|
|
69
|
+
* pull request and the GraphQL read is skipped.
|
|
70
|
+
*
|
|
71
|
+
* **A list read leaves it absent unless asked.** `list` and
|
|
72
|
+
* `listAssociatedWithCommit` fill it only when called with
|
|
73
|
+
* `mergeCommits: true`, at one GraphQL call per 100 merged pull requests.
|
|
74
|
+
*
|
|
75
|
+
* It is never the speculative test-merge commit GitHub computes for an open
|
|
76
|
+
* pull request, so `mergeCommitSha === sha` means "this commit is what the
|
|
77
|
+
* pull request merged as".
|
|
78
|
+
*/
|
|
58
79
|
mergeCommitSha: Schema.optionalKey(Schema.String)
|
|
59
80
|
}) {};
|
|
60
81
|
const AutoMergeResponse = Schema.Struct({});
|
|
@@ -74,6 +95,24 @@ const DisableAutoMerge = GraphQLDocument.make({
|
|
|
74
95
|
}`,
|
|
75
96
|
response: AutoMergeResponse
|
|
76
97
|
})();
|
|
98
|
+
/**
|
|
99
|
+
* One answer per requested id, **in request order**: a merged pull request
|
|
100
|
+
* carries its merge commit, and a node of another type answers `{}`.
|
|
101
|
+
*/
|
|
102
|
+
const MergeCommitsResponse = Schema.Struct({ nodes: Schema.Array(Schema.NullOr(Schema.Struct({ mergeCommit: Schema.optionalKey(Schema.NullOr(Schema.Struct({ oid: Schema.String }))) }))) });
|
|
103
|
+
const MergeCommits = GraphQLDocument.make({
|
|
104
|
+
name: "PullRequestMergeCommits",
|
|
105
|
+
document: `query PullRequestMergeCommits($ids: [ID!]!) {
|
|
106
|
+
nodes(ids: $ids) {
|
|
107
|
+
... on PullRequest { mergeCommit { oid } }
|
|
108
|
+
}
|
|
109
|
+
}`,
|
|
110
|
+
response: MergeCommitsResponse
|
|
111
|
+
})();
|
|
112
|
+
/** GraphQL's `nodes(ids:)` accepts at most this many ids per call. */
|
|
113
|
+
const NODES_PER_QUERY = 100;
|
|
114
|
+
/** How many `nodes(ids:)` calls a list's merge-commit read keeps in flight. */
|
|
115
|
+
const MERGE_COMMIT_CONCURRENCY = 4;
|
|
77
116
|
/** GraphQL spells the merge methods in capitals. */
|
|
78
117
|
const GRAPHQL_MERGE_METHOD = {
|
|
79
118
|
merge: "MERGE",
|
|
@@ -110,48 +149,82 @@ const GRAPHQL_MERGE_METHOD = {
|
|
|
110
149
|
var PullRequest = class PullRequest extends Context.Service()("@effected/github/PullRequest") {
|
|
111
150
|
/** The live service, built over a `GitHubClient`. */
|
|
112
151
|
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
113
|
-
/** An in-memory double; unstubbed
|
|
152
|
+
/** An in-memory double; an unstubbed member dies when run, naming itself. */
|
|
114
153
|
static makeTest = (overrides = {}) => ({
|
|
115
|
-
get: overrides.get ?? (() =>
|
|
116
|
-
list: overrides.list ?? (() =>
|
|
117
|
-
listFiles: overrides.listFiles ?? (() =>
|
|
118
|
-
listAssociatedWithCommit: overrides.listAssociatedWithCommit ?? (() =>
|
|
119
|
-
create: overrides.create ?? (() =>
|
|
120
|
-
update: overrides.update ?? (() =>
|
|
121
|
-
upsert: overrides.upsert ?? (() =>
|
|
122
|
-
merge: overrides.merge ?? (() =>
|
|
123
|
-
addLabels: overrides.addLabels ?? (() =>
|
|
124
|
-
requestReviewers: overrides.requestReviewers ?? (() =>
|
|
125
|
-
setAutoMerge: overrides.setAutoMerge ?? (() =>
|
|
154
|
+
get: overrides.get ?? (() => dies("get")),
|
|
155
|
+
list: overrides.list ?? (() => dies("list")),
|
|
156
|
+
listFiles: overrides.listFiles ?? (() => dies("listFiles")),
|
|
157
|
+
listAssociatedWithCommit: overrides.listAssociatedWithCommit ?? (() => dies("listAssociatedWithCommit")),
|
|
158
|
+
create: overrides.create ?? (() => dies("create")),
|
|
159
|
+
update: overrides.update ?? (() => dies("update")),
|
|
160
|
+
upsert: overrides.upsert ?? (() => dies("upsert")),
|
|
161
|
+
merge: overrides.merge ?? (() => dies("merge")),
|
|
162
|
+
addLabels: overrides.addLabels ?? (() => dies("addLabels")),
|
|
163
|
+
requestReviewers: overrides.requestReviewers ?? (() => dies("requestReviewers")),
|
|
164
|
+
setAutoMerge: overrides.setAutoMerge ?? (() => dies("setAutoMerge"))
|
|
126
165
|
});
|
|
127
166
|
/** {@link PullRequest.makeTest} behind a `Layer`. */
|
|
128
167
|
static layerTest = (overrides = {}) => Layer.succeed(PullRequest, PullRequest.makeTest(overrides));
|
|
129
168
|
};
|
|
130
|
-
const
|
|
131
|
-
throw new Error(`PullRequest.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
132
|
-
};
|
|
169
|
+
const dies = unstubbed("PullRequest.makeTest");
|
|
133
170
|
const project = (raw) => Effect.try({
|
|
134
|
-
try: () =>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
171
|
+
try: () => {
|
|
172
|
+
const merged = raw.merged ?? raw.merged_at != null;
|
|
173
|
+
return PullRequestInfo.make({
|
|
174
|
+
number: raw.number,
|
|
175
|
+
nodeId: raw.node_id,
|
|
176
|
+
url: raw.html_url,
|
|
177
|
+
title: raw.title,
|
|
178
|
+
state: raw.state === "closed" ? "closed" : "open",
|
|
179
|
+
head: raw.head.ref,
|
|
180
|
+
headSha: raw.head.sha,
|
|
181
|
+
base: raw.base.ref,
|
|
182
|
+
baseSha: raw.base.sha,
|
|
183
|
+
draft: raw.draft ?? false,
|
|
184
|
+
merged,
|
|
185
|
+
mergedAt: raw.merged_at == null ? Option.none() : Option.some(DateTime.makeUnsafe(raw.merged_at)),
|
|
186
|
+
...raw.body != null ? { body: raw.body } : {},
|
|
187
|
+
...raw.merge_commit_sha != null && merged ? { mergeCommitSha: raw.merge_commit_sha } : {}
|
|
188
|
+
});
|
|
189
|
+
},
|
|
150
190
|
catch: (error) => GitHubError.decode("PullRequest", "GitHub returned an unexpected pull request", error)
|
|
151
191
|
});
|
|
152
192
|
/** `owner:branch` is what GitHub's `head` filter wants for a cross-fork search. */
|
|
153
193
|
const qualifyHead = (owner, head) => head.includes(":") ? head : `${owner}:${head}`;
|
|
154
194
|
const make = (client) => {
|
|
195
|
+
/**
|
|
196
|
+
* Fill `mergeCommitSha` for every merged pull request that lacks it, from
|
|
197
|
+
* GraphQL, in as few `nodes(ids:)` calls as the batch limit allows.
|
|
198
|
+
*
|
|
199
|
+
* @remarks
|
|
200
|
+
* The REST object stopped carrying `merge_commit_sha`, and the field is an
|
|
201
|
+
* `optionalKey`, so without this it would decode clean as always-absent —
|
|
202
|
+
* a silent break for anything matching a commit against it. Nothing is
|
|
203
|
+
* requested when no pull request in the batch needs it.
|
|
204
|
+
*
|
|
205
|
+
* `nodes(ids:)` answers positionally — one entry per id, in order, `null`
|
|
206
|
+
* for an id that resolves to nothing — so each answer is joined to the id
|
|
207
|
+
* at its index. An answer of the wrong length cannot be joined that way and
|
|
208
|
+
* fails as a decode error rather than pairing shas with the wrong pulls.
|
|
209
|
+
*/
|
|
210
|
+
const withMergeCommits = (pulls) => {
|
|
211
|
+
const ids = pulls.filter((pull) => pull.merged && pull.mergeCommitSha === void 0).map((pull) => pull.nodeId);
|
|
212
|
+
if (ids.length === 0) return Effect.succeed(pulls);
|
|
213
|
+
const batches = [];
|
|
214
|
+
for (let i = 0; i < ids.length; i += NODES_PER_QUERY) batches.push(ids.slice(i, i + NODES_PER_QUERY));
|
|
215
|
+
return Effect.forEach(batches, (batch) => Effect.flatMap(client.graphql(MergeCommits, { ids: batch }), (answer) => answer.nodes.length === batch.length ? Effect.succeed(batch.map((id, index) => [id, answer.nodes[index]?.mergeCommit?.oid])) : Effect.fail(GitHubGraphQLError.decode(MergeCommits.name, `nodes(ids:) answered ${answer.nodes.length} entries for ${batch.length} ids`))), { concurrency: MERGE_COMMIT_CONCURRENCY }).pipe(Effect.map((answers) => {
|
|
216
|
+
const oids = /* @__PURE__ */ new Map();
|
|
217
|
+
for (const [id, oid] of answers.flat()) if (oid !== void 0) oids.set(id, oid);
|
|
218
|
+
return pulls.map((pull) => {
|
|
219
|
+
const oid = pull.mergeCommitSha === void 0 ? oids.get(pull.nodeId) : void 0;
|
|
220
|
+
return oid === void 0 ? pull : PullRequestInfo.make({
|
|
221
|
+
...pull,
|
|
222
|
+
mergeCommitSha: oid
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}));
|
|
226
|
+
};
|
|
227
|
+
const withMergeCommit = (pull) => Effect.map(withMergeCommits([pull]), (pulls) => pulls[0] ?? pull);
|
|
155
228
|
const list = Effect.fn("PullRequest.list")(function* (options) {
|
|
156
229
|
const { owner, repo } = yield* Repo;
|
|
157
230
|
const raw = yield* client.paginate("GET /repos/{owner}/{repo}/pulls", {
|
|
@@ -161,7 +234,8 @@ const make = (client) => {
|
|
|
161
234
|
...options?.head !== void 0 ? { head: qualifyHead(owner, options.head) } : {},
|
|
162
235
|
...options?.base !== void 0 ? { base: options.base } : {}
|
|
163
236
|
}, options?.page);
|
|
164
|
-
|
|
237
|
+
const pulls = yield* Effect.forEach(raw, project);
|
|
238
|
+
return options?.mergeCommits === true ? yield* withMergeCommits(pulls) : pulls;
|
|
165
239
|
});
|
|
166
240
|
const create = Effect.fn("PullRequest.create")(function* (input) {
|
|
167
241
|
const { owner, repo } = yield* Repo;
|
|
@@ -198,7 +272,7 @@ const make = (client) => {
|
|
|
198
272
|
...patch.state !== void 0 ? { state: patch.state } : {},
|
|
199
273
|
...patch.base !== void 0 ? { base: patch.base } : {}
|
|
200
274
|
});
|
|
201
|
-
return yield* project(updated);
|
|
275
|
+
return yield* withMergeCommit(yield* project(updated));
|
|
202
276
|
});
|
|
203
277
|
return {
|
|
204
278
|
list,
|
|
@@ -216,7 +290,7 @@ const make = (client) => {
|
|
|
216
290
|
repo,
|
|
217
291
|
pull_number: number
|
|
218
292
|
});
|
|
219
|
-
return yield* project(raw);
|
|
293
|
+
return yield* withMergeCommit(yield* project(raw));
|
|
220
294
|
}),
|
|
221
295
|
listFiles: Effect.fn("PullRequest.listFiles")(function* (number, options) {
|
|
222
296
|
const { owner, repo } = yield* Repo;
|
|
@@ -243,7 +317,8 @@ const make = (client) => {
|
|
|
243
317
|
repo,
|
|
244
318
|
commit_sha: sha
|
|
245
319
|
}, options?.page);
|
|
246
|
-
|
|
320
|
+
const pulls = yield* Effect.forEach(raw, project);
|
|
321
|
+
return options?.mergeCommits === true ? yield* withMergeCommits(pulls) : pulls;
|
|
247
322
|
}),
|
|
248
323
|
upsert: Effect.fn("PullRequest.upsert")(function* (input) {
|
|
249
324
|
const found = (yield* list({
|
package/PullRequestComment.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 { numericId } from "./internal/ids.js";
|
|
@@ -66,19 +67,17 @@ var CommentRecord = class extends Schema.Class("CommentRecord")({
|
|
|
66
67
|
var PullRequestComment = class PullRequestComment extends Context.Service()("@effected/github/PullRequestComment") {
|
|
67
68
|
/** The live service, built over a `GitHubClient`. */
|
|
68
69
|
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
69
|
-
/** An in-memory double; unstubbed
|
|
70
|
+
/** An in-memory double; an unstubbed member dies when run, naming itself. */
|
|
70
71
|
static makeTest = (overrides = {}) => ({
|
|
71
|
-
create: overrides.create ?? (() =>
|
|
72
|
-
upsert: overrides.upsert ?? (() =>
|
|
73
|
-
find: overrides.find ?? (() =>
|
|
74
|
-
delete: overrides.delete ?? (() =>
|
|
72
|
+
create: overrides.create ?? (() => dies("create")),
|
|
73
|
+
upsert: overrides.upsert ?? (() => dies("upsert")),
|
|
74
|
+
find: overrides.find ?? (() => dies("find")),
|
|
75
|
+
delete: overrides.delete ?? (() => dies("delete"))
|
|
75
76
|
});
|
|
76
77
|
/** {@link PullRequestComment.makeTest} behind a `Layer`. */
|
|
77
78
|
static layerTest = (overrides = {}) => Layer.succeed(PullRequestComment, PullRequestComment.makeTest(overrides));
|
|
78
79
|
};
|
|
79
|
-
const
|
|
80
|
-
throw new Error(`PullRequestComment.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
81
|
-
};
|
|
80
|
+
const dies = unstubbed("PullRequestComment.makeTest");
|
|
82
81
|
const recordOf = (raw) => CommentRecord.make({
|
|
83
82
|
id: numericId(raw.id),
|
|
84
83
|
body: raw.body ?? "",
|