@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/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 };
@@ -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 };