@effected/github 0.1.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/ArtifactMetadata.js +62 -0
- package/Attestation.js +106 -0
- package/CheckRun.js +286 -0
- package/GitBranch.js +171 -0
- package/GitCommit.js +183 -0
- package/GitHubApp.js +363 -0
- package/GitHubClient.js +192 -0
- package/GitHubCommit.js +198 -0
- package/GitHubContent.js +54 -0
- package/GitHubError.js +254 -0
- package/GitHubIssue.js +216 -0
- package/GitHubRelease.js +188 -0
- package/GitHubRepository.js +59 -0
- package/GitTag.js +191 -0
- package/GraphQL.js +183 -0
- package/LICENSE +21 -0
- package/PullRequest.js +300 -0
- package/PullRequestComment.js +132 -0
- package/Repo.js +107 -0
- package/Resilience.js +146 -0
- package/Rest.js +47 -0
- package/TokenPermissions.js +188 -0
- package/WorkflowDispatch.js +124 -0
- package/index.d.ts +2427 -0
- package/index.js +24 -0
- package/internal/headers.js +52 -0
- package/internal/octokit.js +115 -0
- package/internal/paginate.js +54 -0
- package/package.json +53 -0
- package/tsdoc-metadata.json +11 -0
package/GitHubIssue.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { GraphQLDocument } from "./GraphQL.js";
|
|
2
|
+
import { GitHubClient } from "./GitHubClient.js";
|
|
3
|
+
import { Repo } from "./Repo.js";
|
|
4
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/GitHubIssue.ts
|
|
7
|
+
/**
|
|
8
|
+
* An issue, projected to what callers read.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
var IssueInfo = class extends Schema.Class("IssueInfo")({
|
|
13
|
+
number: Schema.Int,
|
|
14
|
+
title: Schema.String,
|
|
15
|
+
state: Schema.Literals(["open", "closed"]),
|
|
16
|
+
/** Label names, normalized from GitHub's `string | { name }` union. */
|
|
17
|
+
labels: Schema.Array(Schema.String),
|
|
18
|
+
url: Schema.String,
|
|
19
|
+
/** The GraphQL node id. */
|
|
20
|
+
nodeId: Schema.String
|
|
21
|
+
}) {};
|
|
22
|
+
/**
|
|
23
|
+
* An issue a pull request closes.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
var LinkedIssue = class extends Schema.Class("LinkedIssue")({
|
|
28
|
+
number: Schema.Int,
|
|
29
|
+
title: Schema.String,
|
|
30
|
+
state: Schema.String,
|
|
31
|
+
url: Schema.String,
|
|
32
|
+
nodeId: Schema.String,
|
|
33
|
+
/**
|
|
34
|
+
* Whether a human wrote the link, rather than GitHub inferring it from the
|
|
35
|
+
* branch or commit messages.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* This is the field the whole document exists for. The version this replaces
|
|
39
|
+
* could not express `userLinkedOnly`, so one consumer re-declared the query
|
|
40
|
+
* with the field aliased twice to get it — the single largest duplicated
|
|
41
|
+
* document in the survey.
|
|
42
|
+
*/
|
|
43
|
+
userLinked: Schema.Boolean
|
|
44
|
+
}) {};
|
|
45
|
+
const IssueNodes = Schema.Struct({
|
|
46
|
+
id: Schema.String,
|
|
47
|
+
number: Schema.Int,
|
|
48
|
+
title: Schema.String,
|
|
49
|
+
state: Schema.String,
|
|
50
|
+
url: Schema.String
|
|
51
|
+
});
|
|
52
|
+
const LinkedIssuesResponse = Schema.Struct({ repository: Schema.Struct({ pullRequest: Schema.Struct({
|
|
53
|
+
allLinked: Schema.Struct({ nodes: Schema.Array(IssueNodes) }),
|
|
54
|
+
manuallyLinked: Schema.Struct({ nodes: Schema.Array(IssueNodes) })
|
|
55
|
+
}) }) });
|
|
56
|
+
const LinkedIssuesDocument = GraphQLDocument.make({
|
|
57
|
+
name: "linkedIssues",
|
|
58
|
+
document: `query ($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
59
|
+
repository(owner: $owner, name: $repo) {
|
|
60
|
+
pullRequest(number: $prNumber) {
|
|
61
|
+
allLinked: closingIssuesReferences(first: 50) { nodes { id number title state url } }
|
|
62
|
+
manuallyLinked: closingIssuesReferences(first: 50, userLinkedOnly: true) { nodes { id number title state url } }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}`,
|
|
66
|
+
response: LinkedIssuesResponse
|
|
67
|
+
})();
|
|
68
|
+
const CrossReferencedResponse = Schema.Struct({ repository: Schema.Struct({ issue: Schema.Struct({ timelineItems: Schema.Struct({ nodes: Schema.Array(Schema.Struct({ source: Schema.optionalKey(Schema.Struct({
|
|
69
|
+
__typename: Schema.optionalKey(Schema.String),
|
|
70
|
+
number: Schema.optionalKey(Schema.Int)
|
|
71
|
+
})) })) }) }) }) });
|
|
72
|
+
const CrossReferencedDocument = GraphQLDocument.make({
|
|
73
|
+
name: "issueCrossReferences",
|
|
74
|
+
document: `query ($owner: String!, $repo: String!, $issueNumber: Int!) {
|
|
75
|
+
repository(owner: $owner, name: $repo) {
|
|
76
|
+
issue(number: $issueNumber) {
|
|
77
|
+
timelineItems(last: 50, itemTypes: CROSS_REFERENCED_EVENT) {
|
|
78
|
+
nodes {
|
|
79
|
+
__typename
|
|
80
|
+
... on CrossReferencedEvent { source { __typename ... on PullRequest { number } } }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}`,
|
|
86
|
+
response: CrossReferencedResponse
|
|
87
|
+
})();
|
|
88
|
+
/**
|
|
89
|
+
* Issues.
|
|
90
|
+
*
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
93
|
+
var GitHubIssue = class GitHubIssue extends Context.Service()("@effected/github/GitHubIssue") {
|
|
94
|
+
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
95
|
+
/** An in-memory double; unstubbed members die naming themselves. */
|
|
96
|
+
static makeTest = (overrides = {}) => ({
|
|
97
|
+
get: overrides.get ?? (() => unstubbed("get")),
|
|
98
|
+
list: overrides.list ?? (() => unstubbed("list")),
|
|
99
|
+
close: overrides.close ?? (() => unstubbed("close")),
|
|
100
|
+
comment: overrides.comment ?? (() => unstubbed("comment")),
|
|
101
|
+
linkedIssues: overrides.linkedIssues ?? (() => unstubbed("linkedIssues")),
|
|
102
|
+
isCrossReferencedBy: overrides.isCrossReferencedBy ?? (() => unstubbed("isCrossReferencedBy"))
|
|
103
|
+
});
|
|
104
|
+
/** {@link GitHubIssue.makeTest} behind a `Layer`. */
|
|
105
|
+
static layerTest = (overrides = {}) => Layer.succeed(GitHubIssue, GitHubIssue.makeTest(overrides));
|
|
106
|
+
};
|
|
107
|
+
const unstubbed = (member) => {
|
|
108
|
+
throw new Error(`GitHubIssue.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
109
|
+
};
|
|
110
|
+
/** GitHub sends a label as either a string or an object; callers want the name. */
|
|
111
|
+
const labelNames = (labels) => labels.flatMap((label) => typeof label === "string" ? [label] : label.name !== void 0 ? [label.name] : []);
|
|
112
|
+
const project = (raw) => IssueInfo.make({
|
|
113
|
+
number: raw.number,
|
|
114
|
+
title: raw.title,
|
|
115
|
+
state: raw.state === "closed" ? "closed" : "open",
|
|
116
|
+
labels: labelNames(raw.labels),
|
|
117
|
+
url: raw.html_url,
|
|
118
|
+
nodeId: raw.node_id
|
|
119
|
+
});
|
|
120
|
+
const make = (client) => ({
|
|
121
|
+
get: Effect.fn("GitHubIssue.get")(function* (number) {
|
|
122
|
+
const { owner, repo } = yield* Repo;
|
|
123
|
+
yield* Effect.annotateCurrentSpan({
|
|
124
|
+
owner,
|
|
125
|
+
repo,
|
|
126
|
+
number
|
|
127
|
+
});
|
|
128
|
+
const raw = yield* client.request("GET /repos/{owner}/{repo}/issues/{issue_number}", {
|
|
129
|
+
owner,
|
|
130
|
+
repo,
|
|
131
|
+
issue_number: number
|
|
132
|
+
});
|
|
133
|
+
return project(raw);
|
|
134
|
+
}),
|
|
135
|
+
list: Effect.fn("GitHubIssue.list")(function* (options) {
|
|
136
|
+
const { owner, repo } = yield* Repo;
|
|
137
|
+
yield* Effect.annotateCurrentSpan({
|
|
138
|
+
owner,
|
|
139
|
+
repo
|
|
140
|
+
});
|
|
141
|
+
return (yield* client.paginate("GET /repos/{owner}/{repo}/issues", {
|
|
142
|
+
owner,
|
|
143
|
+
repo,
|
|
144
|
+
...options?.state !== void 0 ? { state: options.state } : {},
|
|
145
|
+
...options?.labels !== void 0 ? { labels: options.labels.join(",") } : {}
|
|
146
|
+
}, options?.page)).map(project);
|
|
147
|
+
}),
|
|
148
|
+
close: Effect.fn("GitHubIssue.close")(function* (number, reason) {
|
|
149
|
+
const { owner, repo } = yield* Repo;
|
|
150
|
+
yield* Effect.annotateCurrentSpan({
|
|
151
|
+
owner,
|
|
152
|
+
repo,
|
|
153
|
+
number
|
|
154
|
+
});
|
|
155
|
+
yield* client.request("PATCH /repos/{owner}/{repo}/issues/{issue_number}", {
|
|
156
|
+
owner,
|
|
157
|
+
repo,
|
|
158
|
+
issue_number: number,
|
|
159
|
+
state: "closed",
|
|
160
|
+
...reason !== void 0 ? { state_reason: reason } : {}
|
|
161
|
+
});
|
|
162
|
+
}),
|
|
163
|
+
comment: Effect.fn("GitHubIssue.comment")(function* (number, body) {
|
|
164
|
+
const { owner, repo } = yield* Repo;
|
|
165
|
+
yield* Effect.annotateCurrentSpan({
|
|
166
|
+
owner,
|
|
167
|
+
repo,
|
|
168
|
+
number
|
|
169
|
+
});
|
|
170
|
+
return (yield* client.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", {
|
|
171
|
+
owner,
|
|
172
|
+
repo,
|
|
173
|
+
issue_number: number,
|
|
174
|
+
body
|
|
175
|
+
})).id;
|
|
176
|
+
}),
|
|
177
|
+
linkedIssues: Effect.fn("GitHubIssue.linkedIssues")(function* (prNumber) {
|
|
178
|
+
const { owner, repo } = yield* Repo;
|
|
179
|
+
yield* Effect.annotateCurrentSpan({
|
|
180
|
+
owner,
|
|
181
|
+
repo,
|
|
182
|
+
prNumber
|
|
183
|
+
});
|
|
184
|
+
const result = yield* client.graphql(LinkedIssuesDocument, {
|
|
185
|
+
owner,
|
|
186
|
+
repo,
|
|
187
|
+
prNumber
|
|
188
|
+
});
|
|
189
|
+
const manual = new Set(result.repository.pullRequest.manuallyLinked.nodes.map((node) => node.number));
|
|
190
|
+
return result.repository.pullRequest.allLinked.nodes.map((node) => LinkedIssue.make({
|
|
191
|
+
number: node.number,
|
|
192
|
+
title: node.title,
|
|
193
|
+
state: node.state,
|
|
194
|
+
url: node.url,
|
|
195
|
+
nodeId: node.id,
|
|
196
|
+
userLinked: manual.has(node.number)
|
|
197
|
+
}));
|
|
198
|
+
}),
|
|
199
|
+
isCrossReferencedBy: Effect.fn("GitHubIssue.isCrossReferencedBy")(function* (issueNumber, prNumber) {
|
|
200
|
+
const { owner, repo } = yield* Repo;
|
|
201
|
+
yield* Effect.annotateCurrentSpan({
|
|
202
|
+
owner,
|
|
203
|
+
repo,
|
|
204
|
+
issueNumber,
|
|
205
|
+
prNumber
|
|
206
|
+
});
|
|
207
|
+
return (yield* client.graphql(CrossReferencedDocument, {
|
|
208
|
+
owner,
|
|
209
|
+
repo,
|
|
210
|
+
issueNumber
|
|
211
|
+
})).repository.issue.timelineItems.nodes.some((node) => node.source?.__typename === "PullRequest" && node.source.number === prNumber);
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
export { GitHubIssue, IssueInfo, LinkedIssue };
|
package/GitHubRelease.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { GitHubError } from "./GitHubError.js";
|
|
2
|
+
import { GitHubClient } from "./GitHubClient.js";
|
|
3
|
+
import { Repo } from "./Repo.js";
|
|
4
|
+
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/GitHubRelease.ts
|
|
7
|
+
/**
|
|
8
|
+
* A release.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
var ReleaseInfo = class extends Schema.Class("ReleaseInfo")({
|
|
13
|
+
id: Schema.Int,
|
|
14
|
+
tag: Schema.String,
|
|
15
|
+
name: Schema.String,
|
|
16
|
+
body: Schema.String,
|
|
17
|
+
draft: Schema.Boolean,
|
|
18
|
+
prerelease: Schema.Boolean,
|
|
19
|
+
/** The web URL. */
|
|
20
|
+
url: Schema.String,
|
|
21
|
+
/** The templated upload endpoint GitHub hands back for assets. */
|
|
22
|
+
uploadUrl: Schema.String
|
|
23
|
+
}) {};
|
|
24
|
+
/**
|
|
25
|
+
* A file attached to a release.
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
var ReleaseAsset = class extends Schema.Class("ReleaseAsset")({
|
|
30
|
+
id: Schema.Int,
|
|
31
|
+
name: Schema.String,
|
|
32
|
+
/** The browser download URL. */
|
|
33
|
+
url: Schema.String,
|
|
34
|
+
/** Size in bytes. */
|
|
35
|
+
size: Schema.Int
|
|
36
|
+
}) {};
|
|
37
|
+
/**
|
|
38
|
+
* Releases.
|
|
39
|
+
*
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
var GitHubRelease = class GitHubRelease extends Context.Service()("@effected/github/GitHubRelease") {
|
|
43
|
+
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
44
|
+
/** An in-memory double; unstubbed members die naming themselves. */
|
|
45
|
+
static makeTest = (overrides = {}) => ({
|
|
46
|
+
create: overrides.create ?? (() => unstubbed("create")),
|
|
47
|
+
getByTag: overrides.getByTag ?? (() => unstubbed("getByTag")),
|
|
48
|
+
getByTagOption: overrides.getByTagOption ?? (() => unstubbed("getByTagOption")),
|
|
49
|
+
list: overrides.list ?? (() => unstubbed("list")),
|
|
50
|
+
update: overrides.update ?? (() => unstubbed("update")),
|
|
51
|
+
uploadAsset: overrides.uploadAsset ?? (() => unstubbed("uploadAsset")),
|
|
52
|
+
listAssets: overrides.listAssets ?? (() => unstubbed("listAssets"))
|
|
53
|
+
});
|
|
54
|
+
/** {@link GitHubRelease.makeTest} behind a `Layer`. */
|
|
55
|
+
static layerTest = (overrides = {}) => Layer.succeed(GitHubRelease, GitHubRelease.makeTest(overrides));
|
|
56
|
+
};
|
|
57
|
+
const unstubbed = (member) => {
|
|
58
|
+
throw new Error(`GitHubRelease.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
59
|
+
};
|
|
60
|
+
const project = (raw) => ReleaseInfo.make({
|
|
61
|
+
id: raw.id,
|
|
62
|
+
tag: raw.tag_name,
|
|
63
|
+
name: raw.name ?? "",
|
|
64
|
+
body: raw.body ?? "",
|
|
65
|
+
draft: raw.draft,
|
|
66
|
+
prerelease: raw.prerelease,
|
|
67
|
+
url: raw.html_url,
|
|
68
|
+
uploadUrl: raw.upload_url
|
|
69
|
+
});
|
|
70
|
+
const AssetResponse = Schema.Struct({
|
|
71
|
+
id: Schema.Int,
|
|
72
|
+
name: Schema.String,
|
|
73
|
+
browser_download_url: Schema.String,
|
|
74
|
+
size: Schema.Int
|
|
75
|
+
});
|
|
76
|
+
const assetOf = (raw) => ReleaseAsset.make({
|
|
77
|
+
id: raw.id,
|
|
78
|
+
name: raw.name,
|
|
79
|
+
url: raw.browser_download_url,
|
|
80
|
+
size: raw.size
|
|
81
|
+
});
|
|
82
|
+
const make = (client) => {
|
|
83
|
+
const getByTag = Effect.fn("GitHubRelease.getByTag")(function* (tag) {
|
|
84
|
+
const { owner, repo } = yield* Repo;
|
|
85
|
+
yield* Effect.annotateCurrentSpan({
|
|
86
|
+
owner,
|
|
87
|
+
repo,
|
|
88
|
+
tag
|
|
89
|
+
});
|
|
90
|
+
const raw = yield* client.request("GET /repos/{owner}/{repo}/releases/tags/{tag}", {
|
|
91
|
+
owner,
|
|
92
|
+
repo,
|
|
93
|
+
tag
|
|
94
|
+
});
|
|
95
|
+
return project(raw);
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
getByTag,
|
|
99
|
+
create: Effect.fn("GitHubRelease.create")(function* (input) {
|
|
100
|
+
const { owner, repo } = yield* Repo;
|
|
101
|
+
yield* Effect.annotateCurrentSpan({
|
|
102
|
+
owner,
|
|
103
|
+
repo,
|
|
104
|
+
tag: input.tag
|
|
105
|
+
});
|
|
106
|
+
const created = yield* client.request("POST /repos/{owner}/{repo}/releases", {
|
|
107
|
+
owner,
|
|
108
|
+
repo,
|
|
109
|
+
tag_name: input.tag,
|
|
110
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
111
|
+
...input.body !== void 0 ? { body: input.body } : {},
|
|
112
|
+
...input.draft !== void 0 ? { draft: input.draft } : {},
|
|
113
|
+
...input.prerelease !== void 0 ? { prerelease: input.prerelease } : {},
|
|
114
|
+
...input.generateReleaseNotes !== void 0 ? { generate_release_notes: input.generateReleaseNotes } : {}
|
|
115
|
+
});
|
|
116
|
+
return project(created);
|
|
117
|
+
}),
|
|
118
|
+
getByTagOption: Effect.fn("GitHubRelease.getByTagOption")(function* (tag) {
|
|
119
|
+
return yield* getByTag(tag).pipe(Effect.map(Option.some), Effect.catchIf(GitHubError.hasKind("notFound"), () => Effect.succeed(Option.none())));
|
|
120
|
+
}),
|
|
121
|
+
list: Effect.fn("GitHubRelease.list")(function* (options) {
|
|
122
|
+
const { owner, repo } = yield* Repo;
|
|
123
|
+
yield* Effect.annotateCurrentSpan({
|
|
124
|
+
owner,
|
|
125
|
+
repo
|
|
126
|
+
});
|
|
127
|
+
return (yield* client.paginate("GET /repos/{owner}/{repo}/releases", {
|
|
128
|
+
owner,
|
|
129
|
+
repo
|
|
130
|
+
}, options?.page)).map(project);
|
|
131
|
+
}),
|
|
132
|
+
update: Effect.fn("GitHubRelease.update")(function* (id, patch) {
|
|
133
|
+
const { owner, repo } = yield* Repo;
|
|
134
|
+
yield* Effect.annotateCurrentSpan({
|
|
135
|
+
owner,
|
|
136
|
+
repo,
|
|
137
|
+
id
|
|
138
|
+
});
|
|
139
|
+
const updated = yield* client.request("PATCH /repos/{owner}/{repo}/releases/{release_id}", {
|
|
140
|
+
owner,
|
|
141
|
+
repo,
|
|
142
|
+
release_id: id,
|
|
143
|
+
...patch.name !== void 0 ? { name: patch.name } : {},
|
|
144
|
+
...patch.body !== void 0 ? { body: patch.body } : {},
|
|
145
|
+
...patch.draft !== void 0 ? { draft: patch.draft } : {},
|
|
146
|
+
...patch.prerelease !== void 0 ? { prerelease: patch.prerelease } : {}
|
|
147
|
+
});
|
|
148
|
+
return project(updated);
|
|
149
|
+
}),
|
|
150
|
+
uploadAsset: Effect.fn("GitHubRelease.uploadAsset")(function* (release, asset) {
|
|
151
|
+
const { owner, repo } = yield* Repo;
|
|
152
|
+
yield* Effect.annotateCurrentSpan({
|
|
153
|
+
owner,
|
|
154
|
+
repo,
|
|
155
|
+
release: release.id,
|
|
156
|
+
asset: asset.name
|
|
157
|
+
});
|
|
158
|
+
const raw = yield* client.requestDecoded("POST /repos/{owner}/{repo}/releases/{release_id}/assets", {
|
|
159
|
+
owner,
|
|
160
|
+
repo,
|
|
161
|
+
release_id: release.id,
|
|
162
|
+
name: asset.name,
|
|
163
|
+
data: asset.data,
|
|
164
|
+
baseUrl: UPLOADS_BASE_URL,
|
|
165
|
+
headers: { "content-type": asset.contentType }
|
|
166
|
+
}, AssetResponse);
|
|
167
|
+
return assetOf(raw);
|
|
168
|
+
}),
|
|
169
|
+
listAssets: Effect.fn("GitHubRelease.listAssets")(function* (id, options) {
|
|
170
|
+
const { owner, repo } = yield* Repo;
|
|
171
|
+
yield* Effect.annotateCurrentSpan({
|
|
172
|
+
owner,
|
|
173
|
+
repo,
|
|
174
|
+
id
|
|
175
|
+
});
|
|
176
|
+
return (yield* client.paginate("GET /repos/{owner}/{repo}/releases/{release_id}/assets", {
|
|
177
|
+
owner,
|
|
178
|
+
repo,
|
|
179
|
+
release_id: id
|
|
180
|
+
}, options?.page)).map(assetOf);
|
|
181
|
+
})
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
/** Release assets do not go to the API host. */
|
|
185
|
+
const UPLOADS_BASE_URL = "https://uploads.github.com";
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
export { GitHubRelease, ReleaseAsset, ReleaseInfo };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { GitHubClient } from "./GitHubClient.js";
|
|
2
|
+
import { Repo } from "./Repo.js";
|
|
3
|
+
import { Context, Effect, Layer } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/GitHubRepository.ts
|
|
6
|
+
/**
|
|
7
|
+
* Repository settings and coordinates.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
var GitHubRepository = class GitHubRepository extends Context.Service()("@effected/github/GitHubRepository") {
|
|
12
|
+
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
13
|
+
/** An in-memory double; unstubbed members die naming themselves. */
|
|
14
|
+
static makeTest = (overrides = {}) => ({
|
|
15
|
+
settings: overrides.settings ?? Effect.sync(() => unstubbed("settings")),
|
|
16
|
+
updateSettings: overrides.updateSettings ?? (() => unstubbed("updateSettings")),
|
|
17
|
+
defaultBranch: overrides.defaultBranch ?? Effect.sync(() => unstubbed("defaultBranch")),
|
|
18
|
+
nodeId: overrides.nodeId ?? Effect.sync(() => unstubbed("nodeId"))
|
|
19
|
+
});
|
|
20
|
+
/** {@link GitHubRepository.makeTest} behind a `Layer`. */
|
|
21
|
+
static layerTest = (overrides = {}) => Layer.succeed(GitHubRepository, GitHubRepository.makeTest(overrides));
|
|
22
|
+
};
|
|
23
|
+
const unstubbed = (member) => {
|
|
24
|
+
throw new Error(`GitHubRepository.makeTest: ${member} was read but not stubbed — pass an override.`);
|
|
25
|
+
};
|
|
26
|
+
const make = (client) => {
|
|
27
|
+
const settings = Effect.fn("GitHubRepository.settings")(function* () {
|
|
28
|
+
const { owner, repo } = yield* Repo;
|
|
29
|
+
yield* Effect.annotateCurrentSpan({
|
|
30
|
+
owner,
|
|
31
|
+
repo
|
|
32
|
+
});
|
|
33
|
+
return yield* client.request("GET /repos/{owner}/{repo}", {
|
|
34
|
+
owner,
|
|
35
|
+
repo
|
|
36
|
+
});
|
|
37
|
+
})();
|
|
38
|
+
return {
|
|
39
|
+
settings,
|
|
40
|
+
updateSettings: Effect.fn("GitHubRepository.updateSettings")(function* (patch) {
|
|
41
|
+
const { owner, repo } = yield* Repo;
|
|
42
|
+
yield* Effect.annotateCurrentSpan({
|
|
43
|
+
owner,
|
|
44
|
+
repo,
|
|
45
|
+
fields: Object.keys(patch).length
|
|
46
|
+
});
|
|
47
|
+
return yield* client.request("PATCH /repos/{owner}/{repo}", {
|
|
48
|
+
...patch,
|
|
49
|
+
owner,
|
|
50
|
+
repo
|
|
51
|
+
});
|
|
52
|
+
}),
|
|
53
|
+
defaultBranch: Effect.map(settings, (repository) => repository.default_branch),
|
|
54
|
+
nodeId: Effect.map(settings, (repository) => repository.node_id)
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { GitHubRepository };
|
package/GitTag.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { GitHubError } from "./GitHubError.js";
|
|
2
|
+
import { GitHubClient } from "./GitHubClient.js";
|
|
3
|
+
import { Repo } from "./Repo.js";
|
|
4
|
+
import { Context, Effect, Layer, Option, Result, Schema, Stream } from "effect";
|
|
5
|
+
import { SemVer } from "@effected/semver";
|
|
6
|
+
|
|
7
|
+
//#region src/GitTag.ts
|
|
8
|
+
/** How many annotated-tag dereferences to follow before giving up. */
|
|
9
|
+
const MAX_TAG_PEEL = 5;
|
|
10
|
+
/**
|
|
11
|
+
* A tag and the commit it ultimately points at.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
var TagRef = class extends Schema.Class("TagRef")({
|
|
16
|
+
/** The tag name, without `refs/tags/`. */
|
|
17
|
+
tag: Schema.NonEmptyString,
|
|
18
|
+
/** The **commit** sha, with annotated tags already dereferenced. */
|
|
19
|
+
sha: Schema.String
|
|
20
|
+
}) {};
|
|
21
|
+
/**
|
|
22
|
+
* A tag whose name carries a version.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
var SemverTag = class extends Schema.Class("SemverTag")({
|
|
27
|
+
/** The tag name as GitHub has it. */
|
|
28
|
+
tag: Schema.NonEmptyString,
|
|
29
|
+
/** The commit sha. */
|
|
30
|
+
sha: Schema.String,
|
|
31
|
+
/** The version read out of the name. */
|
|
32
|
+
version: SemVer
|
|
33
|
+
}) {};
|
|
34
|
+
/** The default {@link VersionFromTag}. @public */
|
|
35
|
+
const versionFromTag = (tag) => {
|
|
36
|
+
const at = tag.lastIndexOf("@");
|
|
37
|
+
const candidate = at > 0 ? tag.slice(at + 1) : tag;
|
|
38
|
+
const stripped = candidate.startsWith("v") ? candidate.slice(1) : candidate;
|
|
39
|
+
return stripped === "" ? Option.none() : Option.some(stripped);
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Tags.
|
|
43
|
+
*
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
var GitTag = class GitTag extends Context.Service()("@effected/github/GitTag") {
|
|
47
|
+
static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
|
|
48
|
+
/** An in-memory double; unstubbed members die naming themselves. */
|
|
49
|
+
static makeTest = (overrides = {}) => ({
|
|
50
|
+
create: overrides.create ?? (() => unstubbed("create")),
|
|
51
|
+
upsert: overrides.upsert ?? (() => unstubbed("upsert")),
|
|
52
|
+
delete: overrides.delete ?? (() => unstubbed("delete")),
|
|
53
|
+
list: overrides.list ?? (() => unstubbed("list")),
|
|
54
|
+
resolve: overrides.resolve ?? (() => unstubbed("resolve")),
|
|
55
|
+
latestSemver: overrides.latestSemver ?? (() => unstubbed("latestSemver"))
|
|
56
|
+
});
|
|
57
|
+
/** {@link GitTag.makeTest} behind a `Layer`. */
|
|
58
|
+
static layerTest = (overrides = {}) => Layer.succeed(GitTag, GitTag.makeTest(overrides));
|
|
59
|
+
};
|
|
60
|
+
const unstubbed = (member) => {
|
|
61
|
+
throw new Error(`GitTag.makeTest: ${member}() was called but not stubbed — pass an override.`);
|
|
62
|
+
};
|
|
63
|
+
const shortTag = (tag) => tag.replace(/^refs\/tags\//, "").replace(/^tags\//, "");
|
|
64
|
+
const rejectEmpty = (operation, tag) => {
|
|
65
|
+
const short = shortTag(tag).trim();
|
|
66
|
+
return short === "" ? Effect.fail(GitHubError.rejected(operation, 422, "a tag name is required")) : Effect.succeed(short);
|
|
67
|
+
};
|
|
68
|
+
const make = (client) => {
|
|
69
|
+
const create = Effect.fn("GitTag.create")(function* (tag, sha) {
|
|
70
|
+
const { owner, repo } = yield* Repo;
|
|
71
|
+
const short = yield* rejectEmpty("GitTag.create", tag);
|
|
72
|
+
yield* Effect.annotateCurrentSpan({
|
|
73
|
+
owner,
|
|
74
|
+
repo,
|
|
75
|
+
tag: short
|
|
76
|
+
});
|
|
77
|
+
yield* client.request("POST /repos/{owner}/{repo}/git/refs", {
|
|
78
|
+
owner,
|
|
79
|
+
repo,
|
|
80
|
+
ref: `refs/tags/${short}`,
|
|
81
|
+
sha
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
const reset = (tag, sha) => Effect.gen(function* () {
|
|
85
|
+
const { owner, repo } = yield* Repo;
|
|
86
|
+
yield* client.request("PATCH /repos/{owner}/{repo}/git/refs/{ref}", {
|
|
87
|
+
owner,
|
|
88
|
+
repo,
|
|
89
|
+
ref: `tags/${tag}`,
|
|
90
|
+
sha,
|
|
91
|
+
force: true
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
const listStream = (options) => Stream.unwrap(Effect.map(Repo, ({ owner, repo }) => client.paginateStream("GET /repos/{owner}/{repo}/tags", {
|
|
95
|
+
owner,
|
|
96
|
+
repo
|
|
97
|
+
}, options?.page)));
|
|
98
|
+
const list = Effect.fn("GitTag.list")(function* (options) {
|
|
99
|
+
const { owner, repo } = yield* Repo;
|
|
100
|
+
yield* Effect.annotateCurrentSpan({
|
|
101
|
+
owner,
|
|
102
|
+
repo,
|
|
103
|
+
prefix: options?.prefix ?? ""
|
|
104
|
+
});
|
|
105
|
+
const tags = yield* Stream.runCollect(listStream(options));
|
|
106
|
+
const prefix = options?.prefix;
|
|
107
|
+
return tags.filter((entry) => prefix === void 0 || entry.name.startsWith(prefix)).map((entry) => TagRef.make({
|
|
108
|
+
tag: entry.name,
|
|
109
|
+
sha: entry.commit.sha
|
|
110
|
+
}));
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
create,
|
|
114
|
+
upsert: Effect.fn("GitTag.upsert")(function* (tag, sha) {
|
|
115
|
+
const short = yield* rejectEmpty("GitTag.upsert", tag);
|
|
116
|
+
yield* Effect.annotateCurrentSpan({ tag: short });
|
|
117
|
+
yield* create(short, sha).pipe(Effect.catchIf(GitHubError.hasKind("alreadyExists"), () => reset(short, sha)));
|
|
118
|
+
}),
|
|
119
|
+
delete: Effect.fn("GitTag.delete")(function* (tag) {
|
|
120
|
+
const { owner, repo } = yield* Repo;
|
|
121
|
+
const short = yield* rejectEmpty("GitTag.delete", tag);
|
|
122
|
+
yield* Effect.annotateCurrentSpan({
|
|
123
|
+
owner,
|
|
124
|
+
repo,
|
|
125
|
+
tag: short
|
|
126
|
+
});
|
|
127
|
+
yield* client.request("DELETE /repos/{owner}/{repo}/git/refs/{ref}", {
|
|
128
|
+
owner,
|
|
129
|
+
repo,
|
|
130
|
+
ref: `tags/${short}`
|
|
131
|
+
});
|
|
132
|
+
}),
|
|
133
|
+
list,
|
|
134
|
+
resolve: Effect.fn("GitTag.resolve")(function* (tag) {
|
|
135
|
+
const { owner, repo } = yield* Repo;
|
|
136
|
+
const short = yield* rejectEmpty("GitTag.resolve", tag);
|
|
137
|
+
yield* Effect.annotateCurrentSpan({
|
|
138
|
+
owner,
|
|
139
|
+
repo,
|
|
140
|
+
tag: short
|
|
141
|
+
});
|
|
142
|
+
const ref = yield* client.request("GET /repos/{owner}/{repo}/git/ref/{ref}", {
|
|
143
|
+
owner,
|
|
144
|
+
repo,
|
|
145
|
+
ref: `tags/${short}`
|
|
146
|
+
});
|
|
147
|
+
let sha = ref.object.sha;
|
|
148
|
+
let type = ref.object.type;
|
|
149
|
+
for (let peeled = 0; type === "tag"; peeled += 1) {
|
|
150
|
+
if (peeled >= MAX_TAG_PEEL) return yield* Effect.fail(GitHubError.rejected("GitTag.resolve", 422, `tag ${short} nests deeper than ${MAX_TAG_PEEL} levels`));
|
|
151
|
+
const annotated = yield* client.request("GET /repos/{owner}/{repo}/git/tags/{tag_sha}", {
|
|
152
|
+
owner,
|
|
153
|
+
repo,
|
|
154
|
+
tag_sha: sha
|
|
155
|
+
});
|
|
156
|
+
sha = annotated.object.sha;
|
|
157
|
+
type = annotated.object.type;
|
|
158
|
+
}
|
|
159
|
+
if (type !== "commit") return yield* Effect.fail(GitHubError.rejected("GitTag.resolve", 422, `tag ${short} points at a ${type}, not a commit`));
|
|
160
|
+
return sha;
|
|
161
|
+
}),
|
|
162
|
+
latestSemver: Effect.fn("GitTag.latestSemver")(function* (options) {
|
|
163
|
+
const { owner, repo } = yield* Repo;
|
|
164
|
+
yield* Effect.annotateCurrentSpan({
|
|
165
|
+
owner,
|
|
166
|
+
repo,
|
|
167
|
+
prefix: options?.prefix ?? ""
|
|
168
|
+
});
|
|
169
|
+
const extract = options?.extract ?? versionFromTag;
|
|
170
|
+
let best;
|
|
171
|
+
yield* Stream.runForEach(listStream({ page: options?.page }), (entry) => Effect.sync(() => {
|
|
172
|
+
if (options?.prefix !== void 0 && !entry.name.startsWith(options.prefix)) return;
|
|
173
|
+
const raw = extract(entry.name);
|
|
174
|
+
if (Option.isNone(raw)) return;
|
|
175
|
+
const parsed = SemVer.parseResult(raw.value);
|
|
176
|
+
if (Result.isFailure(parsed)) return;
|
|
177
|
+
const version = parsed.success;
|
|
178
|
+
if (version.prerelease.length > 0 && options?.includePrerelease !== true) return;
|
|
179
|
+
if (best === void 0 || version.compare(best.version) === 1) best = SemverTag.make({
|
|
180
|
+
tag: entry.name,
|
|
181
|
+
sha: entry.commit.sha,
|
|
182
|
+
version
|
|
183
|
+
});
|
|
184
|
+
}));
|
|
185
|
+
return Option.fromUndefinedOr(best);
|
|
186
|
+
})
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
export { GitTag, SemverTag, TagRef, versionFromTag };
|